---
title: Dependencies
description: What's installed, how to add a package, and how your dependencies actually reach your visitors.
---

# Dependencies

**Nothing installs.** There is no `pnpm install` step on the platform, on any plan. You declare a
package in `package.json`; Trivial resolves it against the npm registry, pins the exact version, and
records a digest for every file in its closure. Adding a dependency is one line and a save.

## How a dependency reaches your visitors

Your build bundles **no dependency bytes at all**. Instead the platform writes an **import map** into
your published `index.html` - a list of exact `https://esm.sh/...` URLs, each with an `integrity`
digest - and the browser fetches the modules itself:

```html
<script type="importmap">{
  "imports": {
    "react": "https://esm.sh/react@19.2.8/.../react.mjs"
  },
  "integrity": {
    "https://esm.sh/react@19.2.8/.../react.mjs": "sha384-Du/P2pOpc..."
  }
}</script>
```

Four consequences:

- **Your bundle is tiny**, because it contains only your own code.
- **Your visitors make third-party requests.** If you add a `Content-Security-Policy`, it has to
  allow `https://esm.sh`. If your audience is offline, air-gapped or behind an enterprise firewall
  that blocks unknown hosts, your app will not load for them.
- **The bytes are verified** - the browser refuses a digest mismatch.
- **Server code is different.** `handlers/` and `jobs/` are bundled self-contained at publish - a
  published handler never talks to a CDN at runtime.

## Adding a package

1. **Put it in `dependencies`** in `package.json` - not `devDependencies`. Only `dependencies` is
   resolved. A package in `devDependencies` gets no import-map entry and there is no `node_modules`
   to fall back on, so the import fails.
2. **Save `package.json` first, then import it.** A build that runs while the import exists and the
   declaration doesn't fails with `Failed to resolve import` - a hard error, not a warning.
3. **Use a semver range (`^1.2.3`), never an invented exact version.** The platform takes the
   newest version matching your range that is at least **seven days old on npm**. An exact pin to
   something younger matches nothing, and the import fails to resolve.

The plans differ on how fast the build runs
(Plus gets more CPU and higher queue priority), not on when or whether something installs - see
[Limits](./limits.md).

## Already in every scaffold project

From the scaffold's `package.json` - import these directly, nothing to add:

`react` (19) · `react-dom` · `react-router-dom` (v7) · `@radix-ui/react-slot` ·
`@radix-ui/react-toggle` · `class-variance-authority` · `clsx` · `tailwind-merge` ·
`date-fns` · `drizzle-orm` · `hono` · `nanoid` · `zod`

Plus eight UI primitives as plain source in `src/components/ui/` (alert, badge, button, card, input,
skeleton, textarea, toggle) - no package needed.

The scaffold's `devDependencies` (Vite, TypeScript, Tailwind, `@vitejs/plugin-react`,
`vite-plugin-pages`) are there for your editor and for `trivial dev` - the cloud build never reads
them ([The environment](./environment.md)).

## Everything else

Any package on npm that runs in a browser. Popular ones are ordinary: `@tanstack/react-query`,
`axios`, `framer-motion`, `lucide-react`, `react-hook-form` (with `zod` and `@hookform/resolvers`), `recharts`, `zustand`, `three` with
`@react-three/fiber` (v9) and `@react-three/drei` (v10). Add the line, save, import.

**What will never work, at any version:**

- packages that need Node built-ins at runtime (`fs`, `path`, `child_process`),
- packages shipping uncompiled framework sources (`.vue`, `.svelte`),
- packages needing a build-time transform or a Babel macro - no plugin of yours runs at build time.

If a package won't resolve, prefer a browser-native alternative rather than pinning a different
version of the same thing.

**A dependency's own stylesheet does not reach the page.** If a component library's instructions
begin with `import "the-lib/dist/styles.css"`, that stylesheet will not load - a CSS subpath is never
in the import map. Prefer libraries that style with Tailwind classes or inline styles.

## What blocks a publish

Dependency health is checked against npm's advisory data at publish:

- **Malware** in the closure, at any depth, refuses the publish.
- **A vulnerability advisory** never breaks your working preview. A **direct** dependency with an
  uncleared high or critical advisory gates the publish until you move off the affected version;
  a transitive advisory, or one reviewed as unreachable on this platform, is recorded as a warning
  and does not block.

A version that resolves to nothing - an exact pin inside the seven-day hold, a range nothing on npm
satisfies, a name that doesn't exist - is not reported as a version problem. It simply leaves the
package out of the import map, and the build fails with `Failed to resolve import "<package>"`. Check
the range first when you see that. See [Errors](./errors.md).

## Handlers and jobs

Server code in `handlers/` and `jobs/` may import **any package your project declares in
`package.json` `dependencies`**, plus your own project files. Same declaration, same pinned version,
same verified bytes as your pages - so a payments SDK, a mail client or a JWT library works there.

Two things are never importable, and the publish gate refuses both by name, with the rule and the line:

| Refused | Rule |
|---|---|
| Node built-ins - `fs`, `net`, `path`, `child_process`, … with or without the `node:` prefix (`crypto` too - use the global `crypto.subtle`) | `forbidden-import` |
| Raw database drivers - `pg`, `postgres`, `mysql2`, `mongodb`, `ioredis`, … | `raw-db-driver` |

A bare specifier your `package.json` doesn't declare (`undeclared-import`) and `import()` with a
computed specifier (`dynamic-import`) are refused too. The full catalog: [Errors](./errors.md).
