Skip to main content
A plugin is a feature delivered as source you own. Auth, the admin panel, file storage, email, AI chat — none of them are framework internals. Each is a folder of resources, routes, and packages that merges into your codebase when you install it. From that point on, the files are yours to edit, extend, or delete.
Everything is a plugin. Every plugin is your code. Like shadcn/ui, but for your entire stack.

How merging works

Installing a plugin doesn’t add a dependency you import from node_modules — it copies files into the three places your code already lives:
  • apps/api/src/resources/<name>/ — endpoints, methods, handlers, middlewares, schemas
  • apps/web/src/routes/<name>/TanStack Router routes, auto-discovered by file path
  • packages/<pkg>/ — any shared package the plugin ships (e.g. @ship/ai, @ship/emails)
The merge is copy-new-files-only: your curated files always win, so a plugin can only contribute new files, never overwrite yours. Its declared dependencies are appended to the right package.json, and any infra it needs (a docker-compose.<plugin>.yml, an infra:<plugin> script, extra .env lines) is wired in too. After files land, codegen runs so the new resources and routes are live:
pnpm --filter api codegen
That regenerates src/router.ts, src/contract.ts, and src/db.ts to include the plugin’s endpoints and tables. For a fresh scaffold, the CLI runs this for you after install.

Installing plugins

Pick plugins during scaffolding — the init flow shows a multiselect, then merges your choices into the new project:
npx @paralect/ship init
You can also merge a plugin into an existing project from its source:
pnpm plugin:install <git-url>
pnpm plugin:uninstall <name>
pnpm plugin:list
Auth is plugin-delivered. The base apps/web ships only a landing page; the Auth plugin (auth-starter) adds the API wiring, the sign-in/up and reset pages, the authenticated app shell, and the typed oRPC client. A full-stack project that needs accounts starts with this plugin.

The catalog

PluginWhat it addsRequires
auth-starterbetter-auth wiring — email/password, verification, reset, Google OAuth — plus the web auth pages, app shell, and oRPC client.postgres, mailer, cloud-storage
adminAn admin dashboard with a paginated user list at /app/admin. The canonical TanStack Router plugin example.postgres, auth-starter
notesA small notes CRUD resource and UI — the minimal end-to-end plugin example.postgres
ai-chatStreaming AI chat via the @ship/ai package (Google Gemini): conversations, messages, AI responses.postgres, auth-starter
mailerTransactional email — Resend + React Email templates, exposed as @ship/emails.
cloud-storageS3-compatible file storage with a local Garage dev server, exposed as @ship/cloud-storage.
A plugin’s requires are merged in automatically — selecting admin pulls in auth-starter, which pulls in mailer and cloud-storage.

Anatomy of a plugin

A plugin mirrors the resource-owns-everything layout of the app it merges into:
my-plugin/
  plugin.json                 # name, version, requires, dependencies
  api/
    src/resources/things/
      things.schema.ts         # pgTable + Zod → picked up by codegen-db
      endpoints/*.ts           # oRPC endpoints → picked up by codegen-router
      methods/*.ts             # business logic
      handlers/*.ts            # mutation-event handlers (side effects)
      middlewares/*.ts         # per-resource gates (can-edit-*.ts)
  web/
    routes/_authenticated/app/things/
      index.tsx                # → apps/web/src/routes/...
      -components/             # private, ignored by the router
  packages/
    my-lib/                    # → packages/my-lib (workspace:*)
The plugin’s API resources build on the same @/endpoint base and the same @/middlewares/* gates (is-authorized, is-admin, can-access, can-edit) as the rest of your code — there’s nothing plugin-specific to learn. Web routes go under web/routes/** and TanStack Router discovers them by path.

plugin.json

The manifest names the plugin, lists what it requires, and declares which dependencies go to which app:
plugin.json
{
  "name": "ai-chat",
  "version": "1.0.0",
  "description": "AI chat with Google Gemini",
  "dependencies": {
    "api": { "@ship/ai": "workspace:*" }
  }
}
dependencies.api is appended to apps/api/package.json; dependencies.web to apps/web/package.json.

Why this matters

Because a plugin is just your code in the standard layout, there’s no abstraction boundary to fight. You can read every line, set a breakpoint in it, refactor a handler, or delete the feature outright. Coding agents see the same one-obvious-way structure they see everywhere else in the repo — the plugin is indistinguishable from code you wrote by hand, because after install, it is.