# findplate docs > A production-ready full-stack project, generated in one command. # Introduction URL: https://plate.findmalek.engineer/docs Markdown: https://plate.findmalek.engineer/docs/index.md > What findplate generates and how the docs are laid out. findplate is a generator. One command creates a working turborepo with a landing page, a dashboard with login, and an api, wired to a database, auth and typed rpc. You pick a design preset and the optional modules you want, and you get a project you own from the first commit. **bun** ```bash bunx findplate create ``` **pnpm** ```bash pnpm dlx findplate create ``` **npm** ```bash npx findplate create ``` **yarn** ```bash yarn dlx findplate create ``` ## What you get - `apps/web`: a hero landing page, only talks to the api - `apps/dashboard`: better-auth login and an app shell, only talks to the api - `apps/api`: orpc on bun. The only app that touches the database, auth, storage and providers - `packages/*`: `ui` (shadcn), `orpc`, `db` (drizzle and a seed scenario), `auth`, `env`, plus the optional modules - biome for format and lint, oxlint for `@shadcn/lint`, security headers, one root `.env` ## Where to go next - [Quickstart](https://plate.findmalek.engineer/docs/quickstart.md): Generate a project and run it locally. - [CLI reference](https://plate.findmalek.engineer/docs/cli.md): Every flag, and the agent mode. - [Presets](https://plate.findmalek.engineer/docs/presets.md): The design starting points. - [Deploy](https://plate.findmalek.engineer/docs/deploy.md): Vercel or your own server. ## Status findplate is built in the open and tracked as [GitHub issues](https://github.com/FindMalek/findplate/issues). --- # Why findplate URL: https://plate.findmalek.engineer/docs/why Markdown: https://plate.findmalek.engineer/docs/why.md > The philosophy, what is in, what is out and why. Most starters are a repo you clone, rename by hand and then spend a day cutting down. findplate is a generator instead: it copies a template, renames it, and removes everything you did not ask for, so what you get has no dead code and is yours from the first commit. ## Philosophy - **Generate, then own it.** There is no runtime dependency on findplate. The output is a normal turborepo that you edit freely. The cli does not come back to update it. - **The minimum is real.** A project with no optional modules still builds, lints and passes its tests. CI generates every module alone, all of them together and none, and checks each. - **One way to do each thing.** One orm, one auth library, one rpc layer, one linter. Fewer choices means a template that stays tested. - **Optional means removable.** A module is a package, its env keys and the lines that use it. Turn it off and all three are gone, not hidden behind a flag. - **Keys are optional at runtime.** A missing key turns a feature off instead of crashing the app, so a fresh project always starts. ## What is in - A turborepo with a landing page, a dashboard with login and an api - Next.js, oRPC on bun, better-auth, Drizzle on Postgres, shadcn, Tailwind - Biome for format and lint, strict TypeScript, security headers, one root `.env` - Design presets driven by shadcn tokens - Optional [modules](https://plate.findmalek.engineer/docs/modules.md) for storage, AI, email, chat notifications, analytics, tracing, error tracking, secrets and public pages - Deploy paths for [Vercel or your own server](https://plate.findmalek.engineer/docs/deploy.md), PR previews, and provisioning of the pieces around it ## What is out - **A second choice of stack.** No Prisma, no tRPC, no Auth.js, no pages router. If you want one, findplate is the wrong tool. - **Payments, teams and billing.** Every product wants them different, and a half-built version costs more to remove than to write. - **A component library of its own.** UI is shadcn, in your repo, so you edit the components. - **Silent updates.** `findplate update` is a three-way merge you run on purpose. It never touches files you didn't change and lists conflicts instead of guessing, so you still read a diff. - **Hosted services.** The modules that need a server (Umami, GlitchTip, Langfuse and the rest) point at an instance you run, with a [compose file](https://plate.findmalek.engineer/docs/self-hosted.md) each. ## Why these choices | Choice | Why | | --- | --- | | oRPC with a separate api app | One typed contract for the web, the dashboard and any other client, and only one app touches the database | | better-auth | Lives in your database and your code, no hosted tenant to depend on | | Drizzle | Plain SQL migrations you can read and a schema in TypeScript | | shadcn | You own the components, and presets are just tokens | | Biome | One fast tool for format and lint, so the CI step is seconds | | pnpm in the project, bun for the api | pnpm for strict, cacheable installs, bun for a fast api runtime | | Markers instead of templating | The template is real code that is type-checked and tested in this repo, not strings with holes | ## Where to go next - [Quickstart](https://plate.findmalek.engineer/docs/quickstart.md): Generate a project. - [Modules](https://plate.findmalek.engineer/docs/modules.md): What you can switch on. - [FAQ](https://plate.findmalek.engineer/docs/faq.md): Common questions. --- # FAQ URL: https://plate.findmalek.engineer/docs/faq Markdown: https://plate.findmalek.engineer/docs/faq.md > Short answers to common questions. ## Can I add a module after generating? Yes. Run `findplate add ` in the project. It applies only that module's files, env keys and migration, and leaves files you edited alone, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md). The [module pages](https://plate.findmalek.engineer/docs/modules.md) list what each one adds. ## Can I remove a module later? Yes. Run `findplate remove ` in the project. It deletes the files the module owns, strips its code from shared files and drops its packages, but only where you haven't edited them. Edited files are listed and left alone, `.env` is never touched and the database migration is kept by default. See [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). Each module page also has the manual steps. ## Does the project depend on findplate? No. The output is a normal turborepo and nothing at runtime imports findplate. The cli only touches the project when you run `add`, `update` or `doctor`, and they read `.findplate/state.json`. ## Why do I need bun? Only to run the cli with `bunx`, and for the api in the generated project. Install dependencies and run scripts with pnpm. With `npx` or `pnpm dlx` you only need bun if you run the api locally. ## Why pnpm and not npm or yarn? The template uses pnpm workspaces and a lockfile for strict, cacheable installs. The scripts in `package.json` call `pnpm`. Switching package manager is possible but you carry the changes. ## What happens when a key is missing? The feature turns off. Storage and AI return `null` from their factory and the code that uses them checks for it. In notification each channel stays off without its keys and `send` skips it, so the app still starts. ## Can I use a different database? The template is built on Postgres with Drizzle, and local development runs it in Docker with `pnpm db:up`. Another database means changing the `db` package and its migrations. ## Can I change the design after generating? Yes. The preset only sets tokens in `packages/ui`. Edit the css variables, or paste a new theme from [shadcn create](https://ui.shadcn.com/create). ## Where do I report a problem? On [GitHub issues](https://github.com/FindMalek/findplate/issues). Include the command you ran and the output of `findplate --version`. --- # Quickstart URL: https://plate.findmalek.engineer/docs/quickstart Markdown: https://plate.findmalek.engineer/docs/quickstart.md > From zero to a running project in a few minutes. ## Requirements - Node 24 or newer - [pnpm](https://pnpm.io) for the generated project - Docker, for the local Postgres - [bun](https://bun.sh), if you run the cli with `bunx` ## Create a project **bun** ```bash bunx findplate create ``` **pnpm** ```bash pnpm dlx findplate create ``` **npm** ```bash npx findplate create ``` **yarn** ```bash yarn dlx findplate create ``` The prompts ask for a project name, a one-line pitch and a design preset, then offer a profile to start from. Every answer has a flag, see the [CLI reference](https://plate.findmalek.engineer/docs/cli.md). ```text ◆ Profile │ ○ Minimal (just the core, add modules later with findplate add) │ ● Standard (the usual starting set, nothing to host yourself) │ ○ Everything (every module that runs without a server of your own) │ ○ Custom (start from standard and pick by hand) ● Standard picks storage, docs, ai, rate-limit ◆ Modules (space toggles, enter confirms) │ ◻ Product pages │ │ ◼ Docs site │ │ ◻ Waitlist │ └ ◻ Status page │ ◻ Platform │ │ ◼ File storage │ │ ◼ AI │ └ ◼ Rate limiting │ ◻ Notifications │ └ ◻ Notification │ ... ● Notification needs Webhooks, added it ◇ Keys you will need ──────────────────────────────────────────────────╮ │ RESEND_API_KEY Resend API key (email) │ │ https://resend.com/api-keys │ │ GROQ_API_KEY Groq API key https://console.groq.com/keys │ ╰───────────────────────────────────────────────────────────────────────╯ ◆ Resend API key (email) (optional, https://resend.com/api-keys) │ _ ``` Press Enter on any key to skip it. The skipped ones come back as a "Still to set in .env" list when the cli finishes, and `findplate doctor` keeps warning until they are filled. Skip the prompts with `--profile` or `--modules`, see [modules](https://plate.findmalek.engineer/docs/modules.md#profiles). The cli then copies the template, installs dependencies, formats the result with biome and makes the first git commit on `main`. ## Run it ```bash cd my-app pnpm db:up pnpm dev ``` | App | URL | | --- | --- | | web | http://localhost:3000 | | api | http://localhost:3001 | | dashboard | http://localhost:3002 | `pnpm db:up` starts Postgres in docker, on the first free port from 5432. The dashboard admin email is printed when the cli finishes, and the password is `ADMIN_PASSWORD` in your `.env`. ## Next steps - Shape the look: [customizing the theme](https://plate.findmalek.engineer/docs/theming.md) - Add keys for the modules you picked in `.env` - Ship it: [deploy](https://plate.findmalek.engineer/docs/deploy.md) --- # Project structure URL: https://plate.findmalek.engineer/docs/project-structure Markdown: https://plate.findmalek.engineer/docs/project-structure.md > The turborepo the cli generates. **my-app/** ```text ├─ apps/ │ ├─ web/ # hero landing, next │ ├─ dashboard/ # better-auth login and app shell, next │ ├─ api/ # orpc on bun │ └─ docs/ # optional module, fumadocs on next ├─ packages/ │ ├─ ui/ # shadcn components and theme tokens │ ├─ common/ # shared zod schemas, constants and utils │ ├─ orpc/ # procedures and clients, built on common │ ├─ db/ # drizzle schema, migrations, seed scenario │ ├─ auth/ # better-auth setup │ ├─ env/ # zod-validated env, one root .env │ ├─ config/ # shared config │ ├─ storage/ # optional module │ └─ ai/ # optional module ├─ docker/ # local postgres compose file ├─ .env # generated, with secrets filled in └─ turbo.json ``` ## Core and modules The core is fixed and always generated: the apps `api`, `web` and `dashboard`, and the packages `ui`, `orpc`, `db`, `auth`, `env`, `common` and `config`. Everything else is a [module](https://plate.findmalek.engineer/docs/modules.md): storage, AI, docs, notifications, analytics and so on. Modules are grouped, and the `minimal`, `standard` and `everything` profiles are just named sets of them, so you start from one and tweak. ## One rule for the apps `apps/api` is the only app that touches the database, auth, storage and providers. `web` and `dashboard` only call the api, so secrets and data access live in one place and the two frontends stay thin. ## Shared code lives in common `packages/common` holds what more than one app or package needs: zod schemas and their inferred types, constants such as input limits, and small utils. The api validates with the same schemas the dashboard forms read their limits from, so a rule changes in one file. Import from `@your-scope/common/schemas`, `/constants` or `/utils`; don't redeclare a type in an app when one exists there. ## Commands | Command | What it does | | --- | --- | | `pnpm dev` | all apps | | `pnpm dev:web` | web and api | | `pnpm dev:dashboard` | dashboard and api | | `pnpm build` | build everything with turbo | | `pnpm check` | biome format and fix | | `pnpm lint` | biome and oxlint, no fixes | | `pnpm check-types` | typescript across the repo | | `pnpm db:up` / `db:down` | local postgres | | `pnpm db:generate` / `db:migrate` / `db:seed` / `db:studio` | drizzle | | `pnpm ui:add ` | add a shadcn component to `packages/ui` | ## Environment One `.env` at the root feeds every app, validated by `packages/env`. The cli generates it from `.env.example` with secrets filled in. Keys for modules you turned off are removed. --- # CLI reference URL: https://plate.findmalek.engineer/docs/cli Markdown: https://plate.findmalek.engineer/docs/cli.md > Every flag of findplate create, and the mode for scripts and coding agents. ```bash findplate create [dir] [options] ``` `create` is the default command, so `findplate [dir]` works too. ## Flags | Flag | Description | | --- | --- | | `[dir]` | target directory. Also the project name when `--name` is not set | | `--name ` | project name. Becomes the package scope, titles and database name | | `--pitch ` | one-line pitch shown on the generated landing page | | `--preset ` | design preset, see [presets](https://plate.findmalek.engineer/docs/presets.md). Default `monochrome` | | `--profile ` | `minimal`, `standard` or `everything`, see [profiles](https://plate.findmalek.engineer/docs/modules.md#profiles). Default `standard` with `-y` | | `--modules ` | exact modules to include, see [modules](https://plate.findmalek.engineer/docs/modules.md). Wins over `--profile`. Pass an empty value for none | | `--no-install` | skip `pnpm install` | | `--no-git` | skip `git init` and the first commit | | `--no-agents` | skip generating agent rules, see [agents](https://plate.findmalek.engineer/docs/agents.md) | | `--no-skills` | skip installing the recommended public skills | | `--github`, `--no-github` | create the GitHub repo, board and rules after the first commit, see [GitHub setup](https://plate.findmalek.engineer/docs/github.md). Asked when interactive | | `-y`, `--yes` | no prompts, use defaults | | `-h`, `--help` | show help | | `-v`, `--version` | show the version | Project names are lowercase letters, digits and dashes, 3 to 40 characters. The target directory must be missing or empty. ## Interactive mode Without `-y`, in a terminal, the cli asks for: 1. the name, pitch and design preset 2. a profile: Minimal, Standard, Everything or Custom 3. a checklist of modules grouped by purpose, pre-ticked from the profile. Space toggles one module or a whole group. A module that needs another one ticks it for you and says so 4. a short "Keys you will need" note for the modules you kept, each key with the page to get it from 5. the keys themselves, hidden as you type. Every one is optional Keys you skip are listed under "Still to set in .env" when the cli finishes, and `findplate doctor` warns about them later. Ctrl+C at any prompt cancels without creating anything. Re-running `create` in an unfinished project resumes with the profile and modules you chose the first time. ## Agent mode Coding agents and scripts have no tty. Pass `-y` and everything you want as flags, and nothing is ever prompted: ```bash bunx findplate create my-app \ --pitch "Invoices for freelancers" \ --preset monochrome \ --modules storage,ai,docs \ -y ``` - `--name` or `[dir]` is required with `-y` - without `--profile` or `--modules`, `-y` uses the `standard` profile - `--modules` wins when both are given - `--modules ""` generates a project with no optional modules - unknown presets, profiles or modules fail with the list of valid ids Add `--no-install --no-git` when the agent will run those steps itself. ## What it does 1. validates the name and target 2. copies the template, renames `acme` to your project, removes the modules you turned off 3. replaces the theme tokens with the preset's `theme.css` 4. writes `.env` with generated secrets and an admin password 5. `pnpm install`, `pnpm check`, then `git init` with a first commit ## Git The first commit lands on your `init.defaultBranch` (or `main` when unset). The step is skipped, with a note, when git isn't installed or when the target already sits inside a git repository, so a project generated into a monorepo never commits to the outer repo. The GitHub step needs its own repo, so pass `--no-github` there. ## Exit codes | Code | Meaning | | --- | --- | | `0` | done, or you cancelled a prompt | | `1` | a step failed, or `doctor` found a failing check | | `2` | usage error, such as a missing `--name` without a terminal, an unknown preset, profile or module | | `3` | `findplate add` or `findplate remove` refused because the git tree has uncommitted changes, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md) | ## Other commands | Command | What it does | | --- | --- | | `findplate add ` | add modules to a generated project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md) | | `findplate remove ` | remove modules from a generated project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove) | | `findplate doctor` | check env, database, tools, dns and deploys, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md) | | `findplate update` | three-way merge of the newest template into your project, see [updates](https://plate.findmalek.engineer/docs/updates.md) | | `findplate github` | repo, board, labels, milestones and rules on GitHub, see [GitHub](https://plate.findmalek.engineer/docs/github.md) | | `findplate provision` | Dokploy, S3, DNS, turbo cache and backups, see [provision](https://plate.findmalek.engineer/docs/provision.md) | | `findplate config` | per-user settings in `~/.config/findplate/config.json` | Re-running `findplate create` in an unfinished project resumes it from `.findplate/state.json`. --- # Updates URL: https://plate.findmalek.engineer/docs/updates Markdown: https://plate.findmalek.engineer/docs/updates.md > Pull template improvements into an existing project with findplate update, and keep dependencies current. A generated project is yours, so findplate never overwrites it silently. `findplate update` merges only what the template changed since your project was created. ```bash npx findplate@latest update --dry-run npx findplate@latest update ``` ## How it works `.findplate/state.json` records the findplate version, preset and modules the project was created with. On update, findplate: 1. downloads the template of the recorded version from npm 2. renders it, and the newest template, with your project's answers into temp folders 3. compares each file in a three-way merge (`git merge-file`): your file, the old template, the new template | Situation | Result | | --- | --- | | the template did not change the file | left alone, even if you edited it | | you did not edit the file | replaced with the new template version | | you edited it and the template changed it too | merged, conflict markers where both touched the same lines | | the template removed a file you edited, or you deleted a file the template changed | left as it is and listed for you to resolve | | the template added a file | added | The project must be a clean git repository, so `git diff` shows exactly what moved and `git restore .` undoes it. `--force` skips that check. ## Flags | Flag | Description | | --- | --- | | `[dir]` | project directory, defaults to the current one | | `--dry-run` | print the plan, change nothing | | `--force` | update even with uncommitted changes | | `--base-template ` | template of the recorded version, instead of downloading it | After an update, run `pnpm install` and your checks. Projects created before the version was recorded need `--base-template`. ## Dependencies Dependency versions live in the `pnpm-workspace.yaml` catalog. Update everything to the latest with: ```bash pnpm bump-deps ``` ## Update notice The cli checks the npm registry at most once a day and prints a line when a newer version exists. Nothing else is sent. Turn it off with `--no-update-check` or `FINDPLATE_NO_UPDATE_CHECK=1`. It is skipped in CI and when there is no terminal. --- # Add and doctor URL: https://plate.findmalek.engineer/docs/add-doctor Markdown: https://plate.findmalek.engineer/docs/add-doctor.md > Turn on a module in an existing project, and check that the project is healthy. ## findplate add ```bash findplate add ai findplate add storage,notification ./my-app ``` Run it inside a project made by `findplate create`, or pass the project folder. It reads `.findplate/state.json` (name, preset, pitch, enabled modules), regenerates the template with and without the module, and applies only the difference: - new files and folders of the module, including its drizzle migration - changes in shared files (`package.json` scripts and dependencies, the pnpm catalog, `.env.example`, files with `@findplate:` markers), but only where you haven't edited them - missing keys appended to `.env` (asked for when you are in a terminal) - modules the new one `requires` are switched on too Files you changed are never overwritten. They are listed so you can merge by hand, or pass `--force` to take the template version. Run `pnpm install` afterwards, or let `add` do it (skip with `--no-install`). It is idempotent: adding a module that is already on does nothing. The git tree must be clean (`.findplate/` aside), otherwise it exits with code `3`, unless you pass `--force`. | Flag | Description | | --- | --- | | `--force` | allow a dirty git tree and overwrite files you edited | | `--no-install` | skip `pnpm install` | | `-y`, `--yes` | don't ask for api keys | The project was generated from template version `templateVersion` in `.findplate/state.json`. Adding a module with a newer cli also pulls the newer template for the files you haven't edited. ## findplate remove ```bash findplate remove ai findplate remove notification,webhooks ./my-app --dry-run ``` The inverse of `add`. It regenerates the template twice from `.findplate/state.json`, with the module and without it, and applies the difference to your project: - files the module owns are deleted when you haven't edited them; edited ones are listed and left alone - marked code in shared files (`package.json`, the pnpm catalog, `.env.example`, `@findplate:` blocks) is stripped where the file is unmodified; where you edited the file the change is merged and any clash is left as conflict markers (`<<<<<<< yours`) to resolve by hand - the module's packages leave every `package.json` and the catalog - its env keys leave `.env.example`. `.env` is never edited, the keys still in it are printed so you can remove them - `.findplate/state.json` drops the module It refuses (exit `2`) when another enabled module requires the one you remove, for example `notification` needs `webhooks`; name both to remove them together. Like `add`, a dirty git tree exits with code `3` unless you pass `--force`. The database is not touched. The drizzle migration and its journal entry stay in the project, because removing a migration that already ran breaks every database that applied it. To drop the tables, add a new migration. If the migration never ran anywhere, pass `--drop-migrations` to delete it too. | Flag | Description | | --- | --- | | `--dry-run` | print what would change, touch nothing | | `--force` | allow a dirty git tree, delete module files you edited and take the template version of conflicting files | | `--drop-migrations` | also delete the module's migration, only if it never ran | Run `pnpm install` afterwards to prune the lockfile, then `pnpm check-types` for code of yours that still imports the module. ## findplate doctor ```bash findplate doctor [dir] [--json] ``` Prints a table of checks, each `PASS`, `WARN`, `FAIL` or `SKIP`: | Check | Fails when | | --- | --- | | project | no `.findplate/state.json` | | `.env` keys | warns when `.env.example` has keys `.env` lacks | | env schema | a variable the zod schema requires (no `.optional()` or `.default()`) is empty | | module keys | warns when a key of an enabled module is empty in `.env`, with its name and the page to get it from | | database | `DATABASE_URL` doesn't accept a connection | | compose port | `POSTGRES_PORT` differs from the port in `DATABASE_URL` | | git, gh auth | git is missing (fail), `gh` isn't logged in (warn) | | node, pnpm, bun | node older than `engines`, pnpm missing (fail), bun missing (warn) | | dns | the urls in `.env` don't resolve, only once `provision dns` ran | | dokploy, vercel | the configured dokploy url is unreachable (warn); vercel is only reported as linked or not | The exit code is `1` when any check fails, `0` otherwise. `--json` prints `{ "ok": boolean, "results": [...] }` for scripts and agents. --- # Modules URL: https://plate.findmalek.engineer/docs/modules Markdown: https://plate.findmalek.engineer/docs/modules.md > Optional packages the cli switches on or off, grouped, with the profiles that pick them. A module is a package plus the code and env keys that depend on it. Turn one off and the cli deletes its package, its env block and every line between the `@findplate:` and `@end` markers in the template. Everything that is not a module is the [core](https://plate.findmalek.engineer/docs/project-structure.md#core-and-modules), and is always generated. Pick a profile with `--profile minimal|standard|everything`, list exact modules with `--modules storage,ai,docs,email`, or choose in the prompt. `--modules` wins over `--profile`. ## Profiles | Profile | Picks | | --- | --- | | `minimal` | no modules, just the core. Add some later with `findplate add` | | `standard` | `storage`, `ai`, `docs`, `rate-limit`. The default with `-y` | | `everything` | every module that runs without a server of your own | `everything` leaves out `umami`, `openobserve`, `glitchtip`, `infisical` and `langfuse`. They only work once you host the matching server, so switching them on by default would hand you code that does nothing. Add them one by one with `--modules` or in the checklist when you run the server. ## Available Each module has a page with what it adds, its env keys, how to remove it and how to swap it. The last column shows the profiles that include it. ### Product pages | Module | Profiles | What it adds | | --- | --- | --- | | [`i18n`](https://plate.findmalek.engineer/docs/i18n.md) | everything | next-intl in web and dashboard with English and French, a language switcher and hreflang in the sitemap | | [`docs`](https://plate.findmalek.engineer/docs/docs-module.md) | standard, everything | `apps/docs`. A fumadocs site on Next with pages about your project | | [`waitlist`](https://plate.findmalek.engineer/docs/waitlist.md) | everything | email signup on the landing page, signups listed in the dashboard | | [`roadmap`](https://plate.findmalek.engineer/docs/roadmap.md) | everything | public `/roadmap` from the repo's GitHub milestones | | [`status`](https://plate.findmalek.engineer/docs/status.md) | everything | public `/status` with live checks of api, database, web and dashboard | ### Platform | Module | Profiles | What it adds | | --- | --- | --- | | [`storage`](https://plate.findmalek.engineer/docs/storage.md) | standard, everything | `packages/storage`. Uploads to any S3 API: Garage, R2, AWS, MinIO | | [`ai`](https://plate.findmalek.engineer/docs/ai.md) | standard, everything | `packages/ai`. Vercel AI SDK with Groq, OpenAI or Anthropic | | [`rate-limit`](https://plate.findmalek.engineer/docs/rate-limiting.md) | standard, everything | per route and ip limits on the api, in memory or on Upstash Redis | | [`flags`](https://plate.findmalek.engineer/docs/flags.md) | everything | `packages/flags`. Typed feature flags: code defaults, env overrides, rollouts edited in the dashboard | | [`jobs`](https://plate.findmalek.engineer/docs/jobs.md) | everything | `packages/jobs`. Background jobs and cron on your Postgres with pg-boss, optional separate worker | | [`webhooks`](https://plate.findmalek.engineer/docs/webhooks.md) | everything | `packages/webhooks`. Signature checks for inbound webhooks: hmac, svix, secret header | ### Notifications | Module | Profiles | What it adds | | --- | --- | --- | | [`notification`](https://plate.findmalek.engineer/docs/notification.md) | everything | `packages/notification` and `apps/email`. Email, Telegram and WhatsApp behind one notifier, branded react-email templates and a preview app | ### Analytics | Module | Profiles | What it adds | | --- | --- | --- | | [`databuddy`](https://plate.findmalek.engineer/docs/databuddy.md) | everything | cookieless analytics in web and dashboard | | [`umami`](https://plate.findmalek.engineer/docs/umami.md) | custom, needs a server | umami script in web and dashboard | ### Observability and secrets | Module | Profiles | What it adds | | --- | --- | --- | | [`logger`](https://plate.findmalek.engineer/docs/logger.md) | everything | `packages/logger`. Json logs with request ids and redaction in api, web and dashboard | | [`openobserve`](https://plate.findmalek.engineer/docs/openobserve.md) | custom, needs a server | otel traces from the api to OpenObserve | | [`glitchtip`](https://plate.findmalek.engineer/docs/glitchtip.md) | custom, needs a server | sentry sdk in api, web and dashboard, pointed at GlitchTip | | [`infisical`](https://plate.findmalek.engineer/docs/infisical.md) | custom, needs a server | `pnpm dev:secrets` runs dev with infisical | | [`langfuse`](https://plate.findmalek.engineer/docs/langfuse.md) | custom, needs a server | Langfuse traces for AI SDK calls (needs `ai`) | ## What each module needs from you The cli lists this before it asks for keys, and `findplate doctor` warns about any key still empty in `.env`, with the page to get it from. Every key is optional at creation time. | Module | Needs | | --- | --- | | `ai` | one of [Groq](https://console.groq.com/keys), [OpenAI](https://platform.openai.com/api-keys) or [Anthropic](https://console.anthropic.com/settings/keys) | | `notification` | any of a [Resend API key](https://resend.com/api-keys), a bot token from [@BotFather](https://t.me/BotFather) and a [Kapso API key](https://app.kapso.ai); each channel turns on with its keys | | `databuddy` | a [Databuddy client id](https://app.databuddy.cc) | | `umami`, `glitchtip`, `langfuse` | a self-hosted server, plus a website id, DSN or key pair from it | | `openobserve`, `infisical` | a self-hosted server | ## Dependencies between modules `langfuse` requires `ai`, and `notification` requires `webhooks`. The cli turns required modules on for you and says so in the prompt, so `--modules langfuse` generates `ai,langfuse`. The self-hosted ones also have a compose snippet each in [self-hosted tools](https://plate.findmalek.engineer/docs/self-hosted.md). --- # Storage URL: https://plate.findmalek.engineer/docs/storage Markdown: https://plate.findmalek.engineer/docs/storage.md > Uploads to any S3 API. The `storage` module adds `packages/storage`, a small client for any S3 compatible API: Garage, Cloudflare R2, AWS S3 or MinIO. It is on by default. ```bash findplate create my-app --modules storage ``` ## What it adds Files and folders: - `packages/storage` Packages: - `@acme/storage` ## Env keys ```bash S3_ENDPOINT= S3_REGION=auto S3_BUCKET= S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= ``` `createStorage(config)` returns `null` when the config is incomplete, so the feature turns itself off instead of crashing. Only `apps/api` uses it. Self-hosted S3 can be provisioned for you, see [provisioning](https://plate.findmalek.engineer/docs/provision.md). ## Remove Run `findplate remove storage` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `storage` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap The package only speaks the S3 API, so swapping provider is an env change: point `S3_ENDPOINT` at R2, AWS, MinIO or Garage and set `S3_REGION`. Use `auto` for R2. --- # AI URL: https://plate.findmalek.engineer/docs/ai Markdown: https://plate.findmalek.engineer/docs/ai.md > Vercel AI SDK with Groq, OpenAI or Anthropic. The `ai` module adds `packages/ai` and a summarize button in the dashboard. It wires the Vercel AI SDK to Groq, OpenAI or Anthropic. It is on by default. ```bash findplate create my-app --modules ai ``` ## What it adds Files and folders: - `packages/ai` - `apps/dashboard/components/summarize-button.tsx` Packages: - `@acme/ai` - `ai` - `@ai-sdk/anthropic` - `@ai-sdk/groq` - `@ai-sdk/openai` ## Env keys ```bash AI_MODEL=groq:openai/gpt-oss-120b GROQ_API_KEY= OPENAI_API_KEY= ANTHROPIC_API_KEY= ``` `AI_MODEL` is `provider:model`. `resolveModel(id, keys)` registers only the providers that have a key and returns `null` for an unknown or unkeyed one, so a missing key turns the feature off. ## Remove Run `findplate remove ai` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `ai` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap Change `AI_MODEL` and set the matching key, for example `anthropic:claude-sonnet-4-5` with `ANTHROPIC_API_KEY`. To add another provider, install its `@ai-sdk/*` package and register it in `packages/ai/src/index.ts`. [Langfuse](https://plate.findmalek.engineer/docs/langfuse.md) traces the calls. --- # Docs site URL: https://plate.findmalek.engineer/docs/docs-module Markdown: https://plate.findmalek.engineer/docs/docs-module.md > A fumadocs site inside your project. The `docs` module adds `apps/docs`, a Next app on port 3003 that uses your preset's tokens and fonts. It is on by default. ```bash findplate create my-app --modules docs ``` ## What it adds Files and folders: - `apps/docs` Packages: - `fumadocs-core` - `fumadocs-mdx` - `fumadocs-ui` - `@types/mdx` ## Env keys ```bash DOCS_URL=http://localhost:3003 ``` The pages are mdx in `apps/docs/content/docs`: setup, environment, structure, scripts and deploy, written for the generated project. Edit them like any other file. It has its own `Dockerfile`. It is readable by agents out of the box: `/llms.txt`, `/llms-full.txt` and every page as markdown at `.md`, plus a copy button on each page. See [For LLMs and agents](https://plate.findmalek.engineer/docs/llm.md). ## Remove Run `findplate remove docs` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `docs` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap The content is plain mdx with fumadocs, so move it to any docs tool that reads mdx. --- # Umami URL: https://plate.findmalek.engineer/docs/umami Markdown: https://plate.findmalek.engineer/docs/umami.md > Self-hosted web analytics script. The `umami` module loads the [Umami](https://umami.is) script in `apps/web` and `apps/dashboard`. It is off by default. ```bash findplate create my-app --modules umami ``` ## What it adds Files and folders: - `apps/web/components/umami.tsx` - `apps/dashboard/components/umami.tsx` ## Env keys ```bash NEXT_PUBLIC_UMAMI_URL= NEXT_PUBLIC_UMAMI_WEBSITE_ID= ``` Both keys are inlined at `next build`, so pass them as build args on Docker and Dokploy. With the website id empty nothing is tracked. A compose file for the server is in [self-hosted tools](https://plate.findmalek.engineer/docs/self-hosted.md#umami). ## Remove Run `findplate remove umami` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `umami` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap Analytics modules are independent. Generate with `databuddy` instead for cookieless tracking, or keep both. See [Databuddy](https://plate.findmalek.engineer/docs/databuddy.md). --- # Databuddy URL: https://plate.findmalek.engineer/docs/databuddy Markdown: https://plate.findmalek.engineer/docs/databuddy.md > Cookieless analytics. The `databuddy` module adds an `Analytics` component to `apps/web` and `apps/dashboard` that renders the `@databuddy/sdk` script with web vitals on. It is off by default. ```bash findplate create my-app --modules databuddy ``` ## What it adds Files and folders: - `apps/web/components/analytics.tsx` - `apps/dashboard/components/analytics.tsx` Packages: - `@databuddy/sdk` ## Env keys ```bash NEXT_PUBLIC_DATABUDDY_CLIENT_ID= ``` Create a website in [Databuddy](https://app.databuddy.cc) and set its client id. With the id empty nothing is tracked. `NEXT_PUBLIC_*` is inlined at `next build`, so pass it as a build arg in Docker and Dokploy. ## Remove Run `findplate remove databuddy` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `databuddy` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap Use [Umami](https://plate.findmalek.engineer/docs/umami.md) for a self-hosted alternative. The two do not depend on each other. --- # OpenObserve URL: https://plate.findmalek.engineer/docs/openobserve Markdown: https://plate.findmalek.engineer/docs/openobserve.md > Otel traces from the api. The `openobserve` module sends OpenTelemetry traces from `apps/api` to your [OpenObserve](https://openobserve.ai) instance. It is off by default. ```bash findplate create my-app --modules openobserve ``` ## What it adds Files and folders: - `apps/api/src/openobserve.ts` Packages: - `@opentelemetry/api` - `@opentelemetry/sdk-node` - `@opentelemetry/exporter-trace-otlp-http` ## Env keys ```bash OPENOBSERVE_URL= OPENOBSERVE_ORG=default OPENOBSERVE_USER= OPENOBSERVE_PASSWORD= ``` `apps/api/src/openobserve.ts` starts the node sdk with an OTLP http exporter. Without `OPENOBSERVE_URL` it does nothing. A compose file is in [self-hosted tools](https://plate.findmalek.engineer/docs/self-hosted.md#openobserve). ## Remove Run `findplate remove openobserve` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `openobserve` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap The exporter is plain OTLP over http, so any OTLP backend works. Change the exporter url and the `Authorization` header in `apps/api/src/openobserve.ts` to use Grafana Tempo, Honeycomb or similar. --- # GlitchTip URL: https://plate.findmalek.engineer/docs/glitchtip Markdown: https://plate.findmalek.engineer/docs/glitchtip.md > Error tracking with the Sentry SDK. The `glitchtip` module adds the Sentry SDK to `apps/api` (`@sentry/bun`) and to `apps/web` and `apps/dashboard` (`@sentry/nextjs`), pointed at a self-hosted [GlitchTip](https://glitchtip.com). It is off by default. ```bash findplate create my-app --modules glitchtip ``` ## What it adds Files and folders: - `apps/web/instrumentation-client.ts` - `apps/dashboard/instrumentation-client.ts` Lines in `apps/web/instrumentation.ts` and `apps/dashboard/instrumentation.ts`, which stay in every project as the hook other modules share. Packages: - `@sentry/bun` - `@sentry/nextjs` ## Env keys ```bash GLITCHTIP_DSN= NEXT_PUBLIC_GLITCHTIP_DSN= ``` `GLITCHTIP_DSN` is used on the server and `NEXT_PUBLIC_GLITCHTIP_DSN` in the browser, inlined at build. Empty means no events are sent. A compose file is in [self-hosted tools](https://plate.findmalek.engineer/docs/self-hosted.md#glitchtip). ## Remove Run `findplate remove glitchtip` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `glitchtip` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap GlitchTip speaks the Sentry protocol, so a Sentry project DSN works in the same two keys. --- # Infisical URL: https://plate.findmalek.engineer/docs/infisical Markdown: https://plate.findmalek.engineer/docs/infisical.md > Secrets for dev through the infisical cli. The `infisical` module adds `.infisical.json` and a `pnpm dev:secrets` script (`infisical run --env=dev -- turbo dev`) that runs the dev servers with secrets from your [Infisical](https://infisical.com) instance instead of a local `.env`. It is off by default. ```bash findplate create my-app --modules infisical ``` ## What it adds Files and folders: - `.infisical.json` ## Env keys None. Install the cli, run `infisical login`, then `infisical init` to fill the workspace id in `.infisical.json`. `pnpm dev` still reads the root `.env`. A compose file is in [self-hosted tools](https://plate.findmalek.engineer/docs/self-hosted.md#infisical). ## Remove Run `findplate remove infisical` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `infisical` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap To use another secrets manager, change the `dev:secrets` script in `package.json` to its runner, for example `doppler run -- turbo dev`. --- # Langfuse URL: https://plate.findmalek.engineer/docs/langfuse Markdown: https://plate.findmalek.engineer/docs/langfuse.md > LLM tracing for the ai module. The `langfuse` module sends traces of AI SDK calls to [Langfuse](https://langfuse.com), cloud or self-hosted. It requires the [`ai`](https://plate.findmalek.engineer/docs/ai.md) module, which the cli turns on for you. It is off by default. ```bash findplate create my-app --modules langfuse ``` ## What it adds Files and folders: - `apps/api/src/langfuse.ts` Packages: - `@langfuse/otel` - `@langfuse/vercel-ai-sdk` - `@opentelemetry/sdk-node` ## Env keys ```bash LANGFUSE_PUBLIC_KEY= LANGFUSE_SECRET_KEY= LANGFUSE_BASE_URL=https://cloud.langfuse.com ``` `apps/api/src/langfuse.ts` registers the OpenTelemetry span processor. Without the keys nothing is sent. A compose file is in [self-hosted tools](https://plate.findmalek.engineer/docs/self-hosted.md#langfuse). ## Remove Run `findplate remove langfuse` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `langfuse` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap Point `LANGFUSE_BASE_URL` at your own instance. --- # Waitlist URL: https://plate.findmalek.engineer/docs/waitlist Markdown: https://plate.findmalek.engineer/docs/waitlist.md > Email signup on the landing page. The `waitlist` module adds an email form to the hero of `apps/web`, a `waitlist.join` procedure and a Waitlist page in the dashboard where admins see the signups. It is off by default. ```bash findplate create my-app --modules waitlist ``` ## What it adds Files and folders: - `packages/db/src/schema/waitlist.ts` - `packages/db/drizzle/0001_waitlist.sql` - `packages/db/drizzle/meta/0001_snapshot.json` - `packages/orpc/src/routers/waitlist.ts` - `apps/web/components/waitlist-form.tsx` - `apps/dashboard/app/(app)/waitlist` ## Env keys None. Duplicates are ignored. It adds the `waitlist` table through the `0001_waitlist` migration, which is dropped with the module. ## Remove Run `findplate remove waitlist` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `waitlist` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap Signups live in your own database. To forward them to a mailing tool, call its api from `packages/orpc/src/routers/waitlist.ts` after the insert, or use the [email](https://plate.findmalek.engineer/docs/email.md) module to send a confirmation. --- # Roadmap URL: https://plate.findmalek.engineer/docs/roadmap Markdown: https://plate.findmalek.engineer/docs/roadmap.md > A public /roadmap from GitHub milestones. The `roadmap` module adds a public `/roadmap` page to `apps/web` that lists the milestones of `GITHUB_REPO` with their progress. It is off by default. ```bash findplate create my-app --modules roadmap ``` ## What it adds Files and folders: - `packages/orpc/src/routers/roadmap.ts` - `packages/orpc/src/lib/roadmap.ts` - `packages/orpc/src/lib/roadmap.test.ts` - `apps/web/app/roadmap` ## Env keys ```bash GITHUB_REPO= GITHUB_TOKEN= ``` Results are cached for 10 minutes. `GITHUB_TOKEN` is optional, set it for a private repo or a higher rate limit. Both keys are part of the base `.env`. ## Remove Run `findplate remove roadmap` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `roadmap` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap The data comes from `packages/orpc/src/lib/roadmap.ts`. Replace the fetch there to read from another tracker, the page only needs the milestone shape. --- # Status URL: https://plate.findmalek.engineer/docs/status Markdown: https://plate.findmalek.engineer/docs/status.md > A public /status page with live checks. The `status` module adds a public `/status` page to `apps/web`. It calls `status.services`, which checks the database and pings the web and dashboard. It is off by default. ```bash findplate create my-app --modules status ``` ## What it adds Files and folders: - `packages/orpc/src/routers/status.ts` - `apps/web/app/status` ## Env keys ```bash WEB_URL= DASHBOARD_URL= ``` Each check has a 3 second timeout. Both urls are already part of the base `.env`. ## Remove Run `findplate remove status` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. If you'd rather do it by hand: for a new project leave `status` out of `--modules`. In a project that already exists, delete the paths above, drop its packages from the `package.json` files, remove its env block and fix what `pnpm check-types` reports. ## Swap Add a service by adding a check in `packages/orpc/src/routers/status.ts`. For uptime history from outside your own infra, point a hosted monitor at `/status`. --- # Notification URL: https://plate.findmalek.engineer/docs/notification Markdown: https://plate.findmalek.engineer/docs/notification.md > Email, Telegram and WhatsApp behind one notifier, with branded react-email templates and a preview app. The `notification` module adds `packages/notification` and `apps/email`. One `createNotifier(env)` covers three channels, and each one turns on when its keys are set. It is in the `everything` profile and needs [webhooks](https://plate.findmalek.engineer/docs/webhooks.md) for the Telegram route. ```bash findplate create my-app --modules notification ``` ## Channels | Channel | Keys | Notes | | --- | --- | --- | | `email` | `RESEND_API_KEY`, `EMAIL_FROM` | react-email templates sent through [email-sdk](https://email-sdk.dev) and Resend | | `telegram` | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, `TELEGRAM_WEBHOOK_SECRET` | Bot API, a webhook route and a setup command | | `whatsapp` | `KAPSO_API_KEY`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_NOTIFY_TO` | Kapso template messages and a setup command | Every key is optional. A channel without keys is skipped, nothing throws, so features can call the notifier without checking. ## Sending The notifier is part of the oRPC services, so procedures use `context.notification`: ```ts await context.notification.send({ channel: "email", to: user.email, template: "welcome", data: { name: user.name, url }, }) await context.notification.notify("New signup: ada@example.com", { channels: ["telegram"] }) context.notification.channels // ["email", "telegram"] ``` `send` renders a template for one channel and returns `{ channel, sent, id? }`, with `sent: false` when the channel is off. `notify` sends plain text to every configured channel, or the ones you list, and reports failures per channel instead of throwing. ## Templates Templates live in `packages/notification/src/templates`: `verifyEmail`, `resetPassword`, `magicLink`, `welcome`, `waitlistConfirmation`, `adminInvite`, `alert` and `test`. Names and props are typed. Each one has: - an email component built from the shared `Layout`, `Box`, `Label`, `Title`, `CtaButton` and `CodeBlock` primitives - a plain text version, generated from the same component - a short summary, which Telegram renders as Markdown and WhatsApp as one line Add an entry to `templates` in `templates/index.ts` and `send` is typed for it. ## Branding `packages/notification/src/brand.ts` holds the colors, radius and fonts every email reads. `findplate create` fills it from your preset, light and dark, so each preset gets matching emails. Emails follow the system theme with `prefers-color-scheme` and fall back to the light colors where a client ignores it. ## Preview app ```bash pnpm dev:email ``` Opens react-email's preview on `:3004` with every template and sample props. The pages in `apps/email/emails` import the templates from the package, so there is one source. `pnpm email:export` writes static html to `apps/email/out`. ## Auth With email configured, better-auth sends the verification and reset password mail through the notifier, and `sendMagicLink` turns the magic link plugin on. Without it those flows stay off. With [waitlist](https://plate.findmalek.engineer/docs/waitlist.md) on, new signups get a confirmation email. ## Set up Telegram 1. In Telegram open [@BotFather](https://t.me/BotFather), send `/newbot` and copy the token into `.env` as `TELEGRAM_BOT_TOKEN`. 2. Run `pnpm --filter @acme/notification telegram:setup "My App Bot" "Alerts from my app"`. Setup waits for `/start`, writes `TELEGRAM_CHAT_ID` to `.env` and registers `/telegram/webhook` with a secret, which it prints as `TELEGRAM_WEBHOOK_SECRET`. The route rejects calls without the secret in `X-Telegram-Bot-Api-Secret-Token`. Telegram only calls https urls, so run setup again against your deployed `API_PUBLIC_URL`. ## Set up WhatsApp Connect a number in [Kapso](https://app.kapso.ai), set `KAPSO_API_KEY`, `WHATSAPP_PHONE_NUMBER_ID` and `WHATSAPP_BUSINESS_ACCOUNT_ID`, then run `pnpm --filter @acme/notification whatsapp:setup`. It submits the notification template to Meta. Once approved, messages go to `WHATSAPP_NOTIFY_TO`. ## Add your email domain ```bash RESEND_API_KEY=re_... pnpm --filter @acme/notification domain example.com ``` It adds the domain to Resend and prints the dns records. Run it again with `--verify` once they propagate, then set `EMAIL_FROM` to an address on that domain. `onboarding@resend.dev` only delivers to your own Resend account. ## Test it `POST /v1/admin/notification/test` with `{ "channel": "telegram" }` as an admin sends a test message. Email needs a `to` address. ## Remove Run `findplate remove notification` in the project, see [add and doctor](https://plate.findmalek.engineer/docs/add-doctor.md#findplate-remove). It deletes the files the module owns, strips its code from shared files and drops its packages, only where you haven't edited them. By hand: for a new project leave `notification` out of `--modules`. In an existing project delete `packages/notification`, `apps/email` and `packages/orpc/src/routers/notification.ts`, drop `@acme/notification` and fix what `pnpm check-types` reports. --- # Webhooks URL: https://plate.findmalek.engineer/docs/webhooks Markdown: https://plate.findmalek.engineer/docs/webhooks.md > Verify inbound webhooks with hmac, svix or a secret header. The `webhooks` module adds `packages/webhooks`, small helpers to check that a request really came from the provider. It has no dependencies and is off by default. The `notification` module needs it for the Telegram webhook, so picking notification turns it on. ```bash findplate create my-app --modules webhooks ``` ## Helpers All helpers compare in constant time and return a boolean. Verify the raw body text, not re-serialized json. | Helper | Style | | --- | --- | | `verifySecretHeader(request, header, secret)` | the provider sends your secret as-is in a header (Telegram) | | `verifyHmac({ secret, body, signature })` | hmac sha256 of the body, hex or base64, optional `sha256=` prefix (GitHub) | | `verifyTimestamped({ secret, body, signature, timestamp })` | hmac of `.`, rejects anything outside `toleranceSeconds` (default 300) | | `verifySvix({ secret, headers, body })` | svix style headers `svix-id`, `svix-timestamp`, `svix-signature` with a `whsec_` secret | ```ts import { verifySvix } from "@acme/webhooks" const body = await request.text() if (!verifySvix({ secret: env.RESEND_WEBHOOK_SECRET, headers: request.headers, body })) { return Response.json({ message: "Forbidden." }, { status: 403 }) } ``` Add the route in `apps/api/src/server.ts` next to the others, the way `/telegram/webhook` is. --- # Rate limiting URL: https://plate.findmalek.engineer/docs/rate-limiting Markdown: https://plate.findmalek.engineer/docs/rate-limiting.md > Per route and per caller limits on the api, in memory or on Upstash Redis. The `rate-limit` module caps how often one caller can hit the api. It is on by default and has no dependencies. ```bash findplate create my-app --modules rate-limit ``` ## What is limited | Where | Rule | Key | | --- | --- | --- | | every oRPC procedure | 120 per minute | route + user id (signed in) or ip | | `waitlist.join` | 5 per 10 minutes | route + ip | | `POST /auth/*` (sign in, sign up) | 10 per minute | ip | Over the limit an oRPC call answers `TOO_MANY_REQUESTS` (429) with `retryAfterMs` in `data`, and the auth route answers 429 with a `retry-after` header. The rules live in `RATE_LIMITS` in `packages/orpc/src/lib/rate-limit.ts`. To limit one procedure harder, call `enforceRateLimit(context, "route.name", rule)` at the top of its handler. The ip is the first hop of `x-forwarded-for`, so run the api behind a proxy that sets it (Dokploy, Vercel, Cloudflare do). Anonymous calls without an ip, like server components calling the api directly, are not counted. ## Backends By default the counters live in memory as a sliding window. That is per process, fine for one api container. With several replicas share the counters through [Upstash Redis](https://upstash.com) (a fixed window counter): ```bash UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= ``` If Upstash is unreachable the request goes through and the error is logged. --- # Internationalization URL: https://plate.findmalek.engineer/docs/i18n Markdown: https://plate.findmalek.engineer/docs/i18n.md > next-intl in web and dashboard with English and French, a language switcher and hreflang in the sitemap. The `i18n` module adds [next-intl](https://next-intl.dev) to the landing page and the dashboard. English and French ship as an example. It is off by default. ```bash findplate create my-app --modules i18n ``` ## How locales are chosen | App | Language comes from | Why | | --- | --- | --- | | web | the url: `/` is English, `/fr` is French | one url, one language, so search engines index each | | dashboard | the language cookie, else the browser's `Accept-Language` | it is private, there is nothing to index | The web app gets its `[locale]` style routing from `apps/web/proxy.ts`. `/fr/roadmap` is rewritten to the existing `/roadmap` page with an `x-locale` header, `/en/roadmap` redirects to `/roadmap`, and a visitor who chose French before is sent from `/roadmap` to `/fr/roadmap`. No page moves into a `[locale]` folder, so existing routes keep working and removing the module is just deleting files. Reading the language from the request makes every web page rendered per request, `/roadmap` and `/status` included, instead of prerendered at build. ## Messages Messages live in `packages/i18n/src/messages/.json` and are typed from `en.json`: a key that is missing in `fr.json`, or a `t("typo")`, fails `pnpm check-types`. ```tsx import { getTranslations } from "next-intl/server" const t = await getTranslations("hero") t("getStarted") // "Commencer" on /fr ``` Client components use `useTranslations` the same way once a `NextIntlClientProvider` wraps them. The two example strings the module translates are the hero in `apps/web/components/hero.tsx` and the settings row in the dashboard, translate more by moving a string into the json files. ## Add a language 1. add the code to `LOCALES` and its name to `LOCALE_NAMES` in `packages/i18n/src/config.ts` 2. add `messages/.json` with the same keys and register it in `MESSAGES` in `packages/i18n/src/index.ts` The switcher, the proxy, the sitemap and the dashboard pick it up. ## Language switcher `LocaleSwitcher` from `@acme/i18n/switcher` is a dropdown built from `@acme/ui` components. It sits in the web footer, and the dashboard has a language row in Settings (`LocaleSetting`). Each language is shown in its own name. ## SEO `apps/web/app/sitemap.ts` lists every page in every language with `hreflang` alternates and an `x-default`, so `/roadmap` and `/fr/roadmap` point at each other. Email templates are not touched, they stay in English until you translate them. ## Env keys None. --- # Feature flags URL: https://plate.findmalek.engineer/docs/flags Markdown: https://plate.findmalek.engineer/docs/flags.md > Small typed feature flags with code defaults, env overrides and per user rollouts edited in the dashboard. The `flags` module adds `packages/flags`: a few typed flags you can turn on for everyone or for a share of your users, without a vendor. It is off by default. ```bash findplate create my-app --modules flags ``` ## Declare a flag Flags live in one object in `packages/flags/src/index.ts`. The name becomes a typed key everywhere, so a typo is a type error. ```ts export const FLAGS = { betaBanner: { default: false, description: "Show the beta badge in the dashboard" }, newCheckout: { default: false, description: "The redesigned checkout" }, } as const ``` ## Use a flag Server components, in `web` and `dashboard`: ```tsx import { getFlag } from "@/lib/flags" if (await getFlag("newCheckout")) return ``` Client components: ```tsx import { useFlag } from "@acme/orpc/flags" const enabled = useFlag("newCheckout") ``` Inside the api, `context.flags.isOn("newCheckout", context.session?.user.id)`. All three ask the api for `flags.all`, which answers for the signed in user when there is a session. The dashboard forwards the visitor's cookie, so a rollout is decided per user. If the api is down you get the default from code. Flags are public: `flags.all` is readable without a session, so never put a secret in a flag name or description. ## Where a value comes from From strongest to weakest: 1. `FLAGS_OVERRIDE` in the env, for example `betaBanner=on,newCheckout=25`. Values are `on`, `off`, `true`, `false` or a percentage. It beats everything, use it as a kill switch. The dashboard shows these rows as locked. 2. a row in the `feature_flag` table, edited on the **Flags** page of the dashboard (admins only) 3. the `default` in code A rollout of 25 means 25% of signed in users, picked by a stable hash of the flag and the user id, so a user keeps their answer while you raise the number. Anonymous visitors only get a flag that is at 100. Overrides from the database are cached for 5 seconds. ## Env keys ```bash FLAGS_OVERRIDE= ``` ## Swap to the Vercel Flags SDK Nothing here locks you in. To move to [Flags SDK](https://flags-sdk.dev) with Vercel Flags: 1. `pnpm add flags @flags-sdk/vercel` in `apps/web` and `apps/dashboard` 2. declare each flag with `flag({ key: "newCheckout", adapter: vercelAdapter, identify })` in a `flags.ts`, where `identify` returns `{ user: { id } }` from the session 3. replace the body of `getFlag` in `lib/flags.ts` with a call to that flag, and `useFlag` with a prop read from a server component 4. create the flags with `vercel flags create newCheckout --kind boolean`, then drop the `flags` module (`findplate remove flags`) once nothing calls it The names stay the same, so call sites do not change. --- # Jobs URL: https://plate.findmalek.engineer/docs/jobs Markdown: https://plate.findmalek.engineer/docs/jobs.md > Background jobs and cron on your existing Postgres with pg-boss, typed with zod. The `jobs` module adds `packages/jobs`: queues, retries and cron on top of the Postgres you already run, through [pg-boss](https://github.com/timgit/pg-boss). No Redis. It is off by default. ```bash findplate create my-app --modules jobs ``` pg-boss keeps its own `pgboss` schema in the same database and creates or migrates it when the api starts, so there is no drizzle migration to generate. ## Define a job A job is a queue name, a zod payload and a handler. Put it in `packages/jobs/src/jobs/` and add it to `JOBS` in `packages/jobs/src/jobs/index.ts`. ```ts export const welcome = defineJob({ name: "send-welcome", description: "Welcome mail after sign up", payload: z.object({ userId: z.string() }), retry: { limit: 5, delaySeconds: 30, backoff: true }, handler: async ({ userId }, { db }) => { // db is the same drizzle instance the api uses }, }) ``` Enqueue it from the api with the typed `send`, the payload is checked at compile time and again at runtime: ```ts await services.jobs.send(welcome, { userId }) ``` `retry` is `limit` extra attempts after the first failure, waiting `delaySeconds`, doubled each time when `backoff` is on. A job that keeps failing stays visible as `failed` in the admin list. The default is 3 retries from 30 seconds with backoff. ## Cron Add `cron` and `scheduleData` to a definition and the worker registers the schedule on start (cron expressions are evaluated in UTC): ```ts defineJob({ name: "digest", cron: "0 8 * * 1", scheduleData: {}, ... }) ``` The sample `cleanup-sessions` job runs every night at 03:00 and deletes expired sessions. `echo` has a payload and no side effect, handy to see the worker run. Removing a job from the code does not remove its schedule from the database, run `delete from pgboss.schedule where name = ''` once. ## Admin procedures Admins only, also reachable over REST under `/v1`: | Procedure | What it does | | --- | --- | | `jobs.list` | every job with its cron and queued, active and failed counts | | `jobs.enqueue` | `{ name, payload }`, checks the payload against the job's schema and answers `BAD_REQUEST` when it doesn't fit | ## Running the worker By default the api process runs the worker and the cron scheduler itself, which is right for one container and for local dev. To scale or isolate them, turn it off on the api and run the worker apart: ```bash JOBS_WORKER=false # on the api, it then only enqueues ``` - locally: `pnpm dev:worker` - anywhere: `bun apps/api/src/worker.ts` - Docker: the api Dockerfile has a `worker` target, `docker build --target worker -f apps/api/Dockerfile .`. It is the same image without the http server and the health check. - Dokploy: add a second application from the same repo, set the Dockerfile to `apps/api/Dockerfile`, the build target to `worker`, and give it the same env as the api. It exposes no port, so skip the domain. Run as many workers as you like, pg-boss hands each job to one of them and runs each cron tick once. ## Graceful shutdown On `SIGTERM` or `SIGINT` the api stops accepting requests, waits up to 8 seconds for running jobs to finish and exits, which fits the 10 seconds Docker gives a container. Jobs still running after that are failed and retried by another worker, so write handlers that are safe to run twice. ## Env keys ```bash JOBS_WORKER=true ``` --- # Logger URL: https://plate.findmalek.engineer/docs/logger Markdown: https://plate.findmalek.engineer/docs/logger.md > Structured json logs with request ids and secret redaction for the api, web and dashboard. The `logger` module replaces scattered `console.log` calls with one logger. It is off by default and has no dependencies. ```bash findplate create my-app --modules logger ``` ## Why not pino `packages/logger` is small and has no dependencies. pino is great, but its worker thread transports and its bundling quirks cost more in a Bun api and in Next server bundles than they give back for a starter. If you outgrow it, keep the `Logger` interface and swap the body for pino. ## What you get - one json line per entry in production, one readable colored line in dev - `LOG_LEVEL` (`debug`, `info`, `warn`, `error`, default `info`) - redaction: any key that looks like `password`, `secret`, `token`, `authorization`, `cookie`, `api key`, `session` or `dsn` is replaced with `[redacted]`, at any depth, headers included - a request id on every line, read from the inbound `x-request-id` (when it is short and plain) or generated, and returned as the `x-request-id` response header - one `request` line per api call with method, path, status and duration (`/health` only at `debug`) ```ts import { logger } from "@acme/logger" logger.info("order paid", { orderId, token }) // token is redacted const log = logger.child({ job: "digest" }) ``` The request id lives in an `AsyncLocalStorage`, so it reaches every line logged while the request runs without passing it around, and `getRequestId()` returns it if you need it. The api also routes `console.*` through the logger, so existing calls (and libraries) get the same format and the request id. ## Web and dashboard `apps/web/instrumentation.ts` and `apps/dashboard/instrumentation.ts` use the Next instrumentation hook. On the Node runtime they route server `console.*` output through the logger and log every failed render or route handler (`onRequestError`) with its route. ## With glitchtip and openobserve Both are optional and null safe. With `openobserve` on, lines logged inside a traced request carry `trace_id` and `span_id`. With `glitchtip` on they carry the Sentry ones. ## Env keys ```bash LOG_LEVEL=info ``` --- # Presets URL: https://plate.findmalek.engineer/docs/presets Markdown: https://plate.findmalek.engineer/docs/presets.md > Design starting points, one folder each. A preset decides how the generated project looks: shadcn style, color tokens, fonts, radius and the hero accents. Pick one with `--preset ` or in the prompt. ## Available | Preset | Look | | --- | --- | | `monochrome` | Black and white, square corners, IBM Plex and JetBrains Mono. Quiet and technical. This site uses it | | `clay` | Warm terracotta and sand, rounded corners, Fraunces headings. Earthy and friendly | | `editorial` | Ink on paper with a single red accent, near-square corners, Playfair headings. Print feel | | `midnight` | Deep blue-black with a cool electric accent, Space Grotesk headings. Dark first | | `soft` | Pastel orchid and mint, very round corners, Nunito and Quicksand. Gentle and playful | ## Anatomy Each preset is a folder in `presets/`. Adding a preset is adding a folder, the cli lists whatever it finds. ```text presets/monochrome/ ├─ preset.json label, description, shadcn style, base color, fonts └─ theme.css :root and .dark color tokens, radius ``` `preset.json` for monochrome: ```json { "label": "Monochrome", "description": "Black and white, square corners, IBM Plex + JetBrains Mono. Quiet and technical.", "shadcn": { "style": "base-lyra", "baseColor": "neutral" }, "fonts": { "sans": { "family": "IBM_Plex_Sans", "weight": ["400", "500", "600", "700"] }, "mono": { "family": "JetBrains_Mono" }, "heading": { "family": "IBM_Plex_Sans", "weight": ["600", "700"] } } } ``` On `create`, the cli copies `theme.css` into `packages/ui/src/styles/theme.css` and applies the fonts and shadcn style. To change the look after generation, see [customizing the theme](https://plate.findmalek.engineer/docs/theming.md). --- # Customizing the theme URL: https://plate.findmalek.engineer/docs/theming Markdown: https://plate.findmalek.engineer/docs/theming.md > Use shadcn create to design any theme and drop it into your project. Presets are a starting point. Any theme you can build on [ui.shadcn.com/create](https://ui.shadcn.com/create) works, because a preset is the same thing that page produces. ## How a preset maps to shadcn create | shadcn create | preset folder | | --- | --- | | style (for example `lyra`) | `preset.json` `shadcn.style` | | base color | `preset.json` `shadcn.baseColor` | | fonts | `preset.json` `fonts` | | radius and color variables | `theme.css`, the `--radius` and color tokens | ## Apply a theme to a generated project 1. Open [ui.shadcn.com/create](https://ui.shadcn.com/create) and design the theme. 2. Copy the `:root` and `.dark` blocks it gives you. 3. Paste them into `packages/ui/src/styles/theme.css`, replacing the existing tokens. 4. If you changed fonts, edit `apps/web/app/fonts.ts` and `apps/dashboard/app/fonts.ts`. The style and base color live in the `components.json` files. All components read the tokens, so the landing page and the dashboard change together. The dev server hot reloads, no rebuild needed. ## Make it a preset To reuse a theme across projects, or contribute one: 1. Create `presets//`. 2. Put the pasted tokens in `theme.css`. 3. Add a `preset.json` with `label`, `description`, `shadcn` and `fonts`, like [the monochrome one](https://plate.findmalek.engineer/docs/presets.md#anatomy). The cli picks the folder up on the next run, so `--preset ` works with no code change. --- # SEO URL: https://plate.findmalek.engineer/docs/seo Markdown: https://plate.findmalek.engineer/docs/seo.md > Sitemap, robots, canonical urls and json-ld for the landing page. `apps/web` ships with the basics, built on Next's metadata routes. Nothing to turn on, and nothing breaks when optional modules are off. | File | What it does | | --- | --- | | `app/sitemap.ts` | `/sitemap.xml` with `/`, plus `/roadmap` and `/status` when those modules are on | | `app/robots.ts` | `/robots.txt`, allows everything and points at the sitemaps | | `app/layout.tsx` | title template, `siteName` and a self-referencing canonical url, resolved against `WEB_URL` | | `components/json-ld.tsx` | `JsonLd`, renders a `