Where your code goes
A kit release reaches your repository as a git merge (UPGRADING.md), and a merge conflicts
wherever both sides changed the same lines. So each place your product plugs into the kit is a
file of its own, next to the kit code that reads it. The kit created each of these files as an
empty stub, in 1.1.0 or a later release, and no release changes one afterwards: what you add there
never conflicts. A later extension point arrives as a new file.
The product files
Section titled “The product files”| What you add | Your file | Read by |
|---|---|---|
| Tables | packages/db/src/schema/product.ts |
packages/db/src/client.ts and drizzle.config.ts |
| Query helpers | packages/db/src/product.ts |
packages/db/src/index.ts, so they import from @repo/db |
| Dependencies of your server code | packages/product-server/package.json |
apps/api, which depends on it, so the API’s install and turbo’s graph |
| Dependencies of your web code | packages/product-web/package.json |
apps/web, which depends on it, likewise |
Dependencies of your query code in packages/db |
packages/product-db/package.json |
packages/db, which depends on it, likewise |
| Counters for the per-organization plan limits you add | packages/db/src/product-usage.ts |
packages/db/src/usage.ts, so organizations.usage and billing.status |
| A rule of your own for which workspaces a member sees, on top of the team boundary | packages/db/src/product-tenancy.ts |
visibleWorkspace in packages/db/src/tenancy.ts, so every workspace lookup and the tenant guards |
| Migrations | packages/db/drizzle-product/, generated |
bun run db:migrate, after the kit’s |
| Procedures, and the schemas clients share | packages/api-contract/src/product.ts |
the contract, and @repo/api-client, which re-exports the whole file |
| Job kinds customers see | packages/api-contract/src/product-jobs.ts |
JOB_KINDS in the contract, so the Jobs list and jobs.* |
| Webhook event types your product emits | packages/api-contract/src/product-webhooks.ts |
WEBHOOK_EVENT_TYPES in the contract, so endpoints can subscribe to them |
| Handlers | apps/api/src/rpc/product.ts |
apps/api/src/rpc/router.ts |
| Raw HTTP routes for what a procedure cannot be (images, streaming downloads), and the path prefixes the access log and tracing leave out | apps/api/src/product-http.ts |
apps/api/src/app.ts, after the kit’s routes, and index.ts for tracing |
| Bodies your raw routes take above the API’s default, by path prefix | apps/api/src/product-body-limits.ts |
apps/api/src/middleware/body-limit.ts, so app.ts and index.ts, and bun run serve’s proxy |
| Procedures a public demo refuses, with the message an API client gets | apps/api/src/rpc/product-demo.ts |
refuseInDemo in apps/api/src/rpc/base.ts |
What your product stops when the process shuts down (productShutdown) |
apps/api/src/product-lifecycle.ts |
apps/api/src/lifecycle.ts, as the drain begins |
Background job kinds (productJobs) and recurring tasks (productSchedules) |
apps/api/src/jobs/product.ts |
the worker: apps/api/src/jobs/handlers.ts and schedules.ts |
Long-running loops each worker process runs (productLoops) |
apps/api/src/jobs/product-loops.ts |
the worker, apps/api/src/jobs/worker.ts, which starts them with it and stops them at shutdown |
| Kinds of stored file: attachments, logos, avatars, exports | apps/api/src/files/product.ts |
files.*, through apps/api/src/files/registry.ts |
| Email templates, in English and German | packages/email/src/product.ts |
templates in @repo/email, next to the kit’s |
| Environment variables, the rules across them, and which of them are secrets | packages/env/src/product.ts |
packages/env/src/index.ts, so env from @repo/env and the start-up checks |
| Your environment rows, explained, and the secrets setup generates | .env.product.example |
bun run setup (into .env), infra/scripts/provision.sh, bun run doctor |
| Seed data for an account the kit creates: the demo owner and the browser suites’ user | packages/auth/scripts/product-seed.ts |
bun run seed and bun run test:e2e:full (packages/auth/scripts/e2e-user.ts) |
| What your product ends when a session ends: a token signed for it, a grant | packages/auth/src/product-sessions.ts |
packages/auth/src/session-events.ts, from Better Auth’s session delete hook in server.ts |
Permission statements, what each role may do, and which of them an API key may hold (productApiKeyScopes) |
packages/auth/src/product-permissions.ts |
packages/auth/src/permissions.ts, so orgScoped, hasPermission and apiKeyScopes too |
| Organization roles of your own, beyond owner, admin and member, with everything each one grants | packages/auth/src/product-roles.ts |
packages/auth/src/permissions.ts, so the role pickers on /app/organization/members, invitations and every permission check |
| Signed-in procedures on a user’s own rows that take an id, and why they need no tenant guard | apps/api/src/rpc/product-tenancy-allowlist.ts |
apps/api/src/rpc/tenancy.test.ts |
Top-level pages under /app/ a workspace slug must not take |
packages/api-contract/src/product-workspace.ts |
RESERVED_WORKSPACE_SLUGS |
| Value lists your tables and your contract share: statuses, kinds, roles | packages/api-contract/src/product-values.ts |
@repo/api-contract/values, which packages/db imports, so a CHECK constraint and a z.enum() read one array |
| Messages, in English and German | apps/web/messages/{en,de}/product.json, and product-<area>.json for an area with many |
Paraglide, as m.product_<page>_<element>(); i18n:compile and vite list each file in project.inlang/settings.json |
| Your own wording for a kit message | apps/web/messages/{en,de}/product-overrides.json, which you create: the kit ships none |
Paraglide, after every other catalogue, so it wins |
Copy for your contract’s refusals (params: { reason }) |
apps/web/src/lib/product-forms.ts |
issueMessage in apps/web/src/lib/forms.ts, so every zodForm |
| Links in the navigation and the command palette | apps/web/src/lib/product.ts |
the app shell’s sidebar and CommandPalette.svelte, after Home |
| Sections of the settings dialog: pages people configure rather than use | apps/web/src/lib/product-settings.ts |
apps/web/src/lib/settings.ts, so the settings dialog, after the kit’s sections, and the command palette |
| Commands in the command palette: actions, entries that depend on the page | apps/web/src/lib/product-commands.ts |
CommandPalette.svelte, after its navigation |
| A step when someone signs out of this browser: revoke a token you issued, clear your pages’ storage | apps/web/src/lib/product-sign-out.ts |
signOut() in apps/web/src/lib/sign-out.ts, so every sign-out in the app |
| Origins your pages load from (a tile server, a font CDN) for the Content-Security-Policy | apps/web/product-csp.ts |
apps/web/csp-sources.ts, so csp() in apps/web/vite.config.ts and the shells’ apps/native/scripts/tauri-config.ts |
| Tabs on a workspace’s pages | apps/web/src/lib/product-workspace.ts |
the workspace layout, before Settings |
| Where a link to a workspace opens, and the kit’s workspace pages your product leaves out of its tabs | apps/web/src/lib/product-workspace-pages.ts |
WorkspaceNav.svelte, the home page, CommandPalette.svelte and the workspace layout |
| Labels for your job kinds, plan limits, plan features, roles and audit-log categories, in English and German | apps/web/src/lib/product-labels.ts |
apps/web/src/lib/jobs.ts, plans.ts, i18n.ts and AuditTable.svelte, so the Jobs list, /app/billing, the role pickers and the audit log’s filter |
| Release notes, one file per release | apps/site/src/content/changelog/ |
the site’s /changelog |
| Blog posts, one file per post | apps/site/src/content/blog/ |
the site’s /blog and /rss.xml |
| Documentation for your users | apps/docs/src/content/product/ |
the docs build, which publishes it by default |
Steps of your own before and after each deploy, one *.sh per step |
infra/scripts/hooks/pre-deploy.d/ and post-deploy.d/ |
infra/scripts/deploy.sh, in name order (infra/scripts/hooks/README.md) |
| Debian packages your API needs at run time | apps/api/product-apt-packages.txt |
apps/api/Dockerfile, which installs them into the API’s image |
Path prefixes, rows, checks and processes of yours for bun run serve |
scripts/serve/product.ts |
scripts/serve/hooks.ts, so the origin proxy, start, preflight and doctor -- --served |
| Notices for your dependencies’ licences | THIRD-PARTY.product.md |
people, and bun run licenses --strict, which checks it names each package you acknowledge |
| The packages whose licence you acknowledge, and licences a package declares wrongly | scripts/product-licenses.ts |
bun run licenses (scripts/licenses.ts) |
| Architecture decisions about your product | docs/product-decisions.md |
people and agents (AGENTS.md names it) |
The rest of a feature goes in new files of yours, which never conflict either: a schema file
exported from schema/product.ts, a contract file spread into productContract, a router added
to productRouter, its pages under apps/web/src/routes/, and their tests.
Your first tenant-scoped feature builds one end to end.
Some of these files have a guide of their own:
- Email templates: Adding a template.
- Raw HTTP routes: HTTP routes of your own.
- Bodies of your own: HTTP routes of your own.
- What ends with a session: HTTP routes of your own.
- Shutdown: Operations → Stopping.
- Value lists: One list for a CHECK.
- Commands and the sign-out step: App shell.
- Settings sections: Settings of your own.
- Copy for your contract’s refusals: Forms.
- Serving on one machine: Your product’s part.
- Dependencies: Dependencies of your own.
- Licences: Licences of your dependencies.
- Debian packages: System packages for the API image.
- Seed data: Seed data.
- Job kinds: Adding a kind.
- Recurring tasks: Schedules.
- Loops: Loops.
- File kinds: Stored files.
- What an API key may hold: API keys.
- Webhook events: Adding an event of your own.
- Plan limits and their counters and labels: Adding a limit.
- Plan features and their labels: Selling a feature on a plan.
- Roles of your own: Roles of your own.
- Who sees which workspace: A visibility rule of your own.
- Where a workspace opens: Where a workspace opens.
Four rules keep these files working:
- Import the kit’s modules, never the file that imports yours. The contract’s
index.tsimportsproduct.ts, so a contract file of yours builds on./procedures, which holds the builders and the text rules, never on./index.SlugSchemaandCursorSchemaare in./procedurestoo. A query helper imports./clientand./schema, never./index; so does a visibility rule inpackages/db/src/product-tenancy.ts, whichtenancy.tsimports;packages/env/src/product.tsimports./configuration-issueand./parse, never./index;packages/auth/src/product-sessions.tsimports./session-events, never./server; andscripts/serve/product.tsimports./types, never./hooks. Importing back is a cycle that fails at start-up. Tests of your environment may importpackages/env/src/testing.ts, which imports nothing of the package.apps/api/src/product-body-limits.tsimports nothing at all:bun run servereads it in a process of its own, before the API starts.packages/api-contract/src/product-values.ts, and any file it re-exports, imports no package:packages/dbloads it through@repo/api-contract/values, andvalues.test.tsfails when it does. The kit’s specs mock the hooks the app’s shell reads,product.ts,product-commands.ts,product-sign-out.tsandproduct-settings.ts, so those may import what your pages need. They mockproduct-workspace.tsandproduct-workspace-pages.tstoo.product-forms.tsis loaded as it is, by the kit’s specs too: keep it to messages and types.product-labels.tsis loaded the same way, under the same rule. - Names of your own. The contract, the router and the environment take your entries after the
kit’s, and a key the kit already uses fails
bun run check(an environment variable’s message names it andproduct.ts, and the API stops at start-up too). So does a resource inproduct-permissions.tsnamed like one of the kit’s or Better Auth’s, such asworkspace,memberorteam, and a role inproduct-roles.tsnamedowner,adminormember. A path prefix of yours, quiet or capped, that covers or sits under one of the kit’s routes (apps/api/src/route-prefixes.ts) stops the API at start-up, and a served one stopsbun run serve. - Values that work as written.
bun run setupappends a row of.env.product.examplethat your.envlacks with the value written there, andinfra/scripts/provision.shappends every row to a new server’s.env, so each value must be safe to append to a real.envas it is. Leave a row empty to take the defaultpackages/env/src/product.tsgives it, and give an example path or address in the comment above it; a secret ships aschange-me…, which both generate, and a key a provider issues ships empty. - The kit never names a file of its own
product.ts,product-*.ts,product.json,product-*.json,.env.product.example,THIRD-PARTY.product.md,docs/product-decisions.mdorproduct-apt-packages.txt, never adds topackages/product-server,packages/product-weborpackages/product-db, and never adds todrizzle-product/, the site’schangelog/andblog/,src/content/product/or the two deploy hook folders. Anything with such a name or in such a folder is yours. The kit tests each hook with a fixture, in a test named after the kit file that reads it (csp-sources.spec.ts,licenses.test.ts), and a kit test that needs a stub as shipped takes it fromscripts/shipped-stubs.fixture.ts, never your copy, so it passes whatever you put in yours; a test of what you put in a hook is yours: name itproduct-*or put it beside your code. In the rootscripts/, which belongs to no package,bun run testruns everyscripts/product*.test.tswith the kit’s own script tests. A stub’s comment says what the file is and which section of this page explains it; the details live here, where a release may correct them.
Dependencies of your own
Section titled “Dependencies of your own”apps/api depends on packages/product-server, apps/web on packages/product-web and
packages/db on packages/product-db, three empty packages of yours. A dependency of your server
code goes in the first, one of your pages in the second, and one your query helpers in
packages/db/src/ import in the third, never in a kit manifest, which a release changes:
bun add --cwd packages/product-server sharpA workspace package of your own goes there the same way, as "@acme/media": "workspace:*", then
bun install. Code in apps/api imports it directly (import sharp from 'sharp'): the root
node_modules holds it, because bunfig.toml hoists, and turbo orders and caches the app after the
package, so a change there runs the app’s checks and tests again. With linker = "isolated", list
the dependency in the app that imports it instead.
No other kit package (auth, env, …) depends on these, so a package of yours in the first two
may depend on @repo/db without a cycle; code that needs both lives in your package or in
apps/api. A package in packages/product-db must not depend on @repo/db, which depends on it:
turbo refuses the cycle. Put there what your query code shares with the rest of your product, such
as a package of your own that both packages/db and the API read, and keep it free of the database.
The API’s and the web app’s images copy packages/ before they install, so their dependencies
reach the images with no Dockerfile change. Bun installs nothing at run time here
(install.auto = "disable" in bunfig.toml): a package no manifest lists fails with
Cannot find package.
Licences of your dependencies
Section titled “Licences of your dependencies”bun run licenses reads every installed package’s declared licence, and every crate the desktop
shell’s Cargo.lock holds, through cargo metadata. Record a dependency of yours
in THIRD-PARTY.product.md, not THIRD-PARTY.md, which is the kit’s. When its licence needs a
notice, write the notice there and put the package’s name, or the prefix its platform builds share
(@img/sharp-), in productAcknowledged in scripts/product-licenses.ts: the scan still prints
it, marked “noted in THIRD-PARTY.product.md”, and --strict passes it. When a package declares one
licence and ships another (a wrapper that bundles a program under other terms), correct it in
productLicenseOverrides:
export const productLicenseOverrides: Readonly<Record<string, LicenseOverride>> = { 'some-wrapper': { license: 'Artistic-1.0-Perl OR GPL-1.0-or-later', reason: 'bundles a Perl program', },};The scan then reads that expression, printed with the declared one and your reason. An override
needs both, and neither list takes a bare scope. --strict fails when an acknowledged package’s
notice file does not name it, when an override matches no installed package, and when it changes
nothing: every package it matches now declares what it says, so delete it. With a half of the scan
left out (--skip=python), an override that matches nothing is only reported.
Deleting an override, by hand or in a merge, reads the declared licence again, and a permissive
one passes. A test of yours keeps the decision: bun run test runs scripts/product*.test.ts, so
scripts/product-licenses.test.ts can scan the installed packages and check what they read as.
import { expect, it } from 'bun:test';import { join } from 'node:path';import { parseLicenseArgs, scanLicenses } from './licenses';
it('reads some-wrapper under the licence it ships', async () => { const lines: string[] = []; const root = join(import.meta.dir, '..'); await scanLicenses(root, parseLicenseArgs(['--all', '--skip=python,rust']), (l) => lines.push(l)); expect(lines.join('\n')).toMatch(/some-wrapper@\S+ — Artistic-1\.0-Perl OR GPL-1\.0-or-later/);});System packages for the API image
Section titled “System packages for the API image”A dependency that runs a program of the system needs that program in the API’s image: a Perl
library’s perl, an image library’s codecs. List each Debian package in
apps/api/product-apt-packages.txt, one per line, with # for comments:
# ExifTool, which exiftool-vendored runsperlapps/api/Dockerfile installs them with apt-get install --no-install-recommends before it copies
the code, so a code change does not run apt again. Each word is a package name, never a command or
a glob, and a package Debian does not have fails the build. Check a new package in a container
before you deploy:
docker build -f apps/api/Dockerfile -t api-check . && docker run --rm api-check perl -v. Running
the API on your own machine (bun run dev, bun run serve), install the package there yourself.
The worker runs the same image.
Seed data
Section titled “Seed data”productSeed() in packages/auth/scripts/product-seed.ts writes your rows for an account the kit
has just created, and returns lines to print:
bun run test:e2e:fullcalls it withpurpose: 'e2e'for the verified user the run’s suites sign in as, on a database made for the run. Give the suites what they need to find: a few records, a file under aTEST_DATA_*path.bun run seedcalls it withpurpose: 'demo'fordemo@example.com, after the kit’s demo rows. It runs on every seed, the account’s first or not, so make it idempotent: look before you insert. A seed of yours that throws on the account’s first run undoes the kit’s rows too.
import { db, eq, notes } from '@repo/db';
export async function productSeed({ user, purpose }: ProductSeedContext) { const [existing] = await db.select().from(notes).where(eq(notes.authorId, user.id)).limit(1); if (existing) return []; await db.insert(notes).values({ id: crypto.randomUUID(), authorId: user.id, title: 'Welcome' }); return [`notes 1 for ${user.email} (${purpose})`];}Contract and handlers
Section titled “Contract and handlers”A procedure is declared in the contract and implemented once, as the kit’s are, with a route so
that it is also REST under /api/v1 and in the OpenAPI document:
import { z } from 'zod';import { protectedProcedure } from './procedures';
export const ReportSchema = z.object({ id: z.string(), title: z.string() });export type Report = z.infer<typeof ReportSchema>;
export const productContract = { reports: { list: protectedProcedure .route({ method: 'GET', path: '/reports', tags: ['reports'], summary: 'Your reports' }) .output(z.array(ReportSchema)), },};import { listReports } from '@repo/db';import { authed } from './base';
export const productRouter = { reports: { list: authed.reports.list.handler(({ context }) => listReports(context.session.user.id)), },};bun run check fails while a procedure in productContract has no handler in productRouter.
The web app imports ReportSchema and Report from @repo/api-client like any kit export, and
calls api.reports.list(). A kit export with the same name as one of yours wins there, without an
error, so name yours apart.
Database: two migration histories
Section titled “Database: two migration histories”The kit’s migrations are packages/db/drizzle; yours are packages/db/drizzle-product, each with
its own history table. bun run db:generate writes yours from what schema/product.ts exports,
and bun run db:migrate applies the kit’s history, then yours. A kit release that adds a migration
therefore never touches yours. UPGRADING.md (“Migrations”) covers the order on fresh and existing
databases, and what to do when db:migrate reports one still pending.
A table of yours can reference a kit table: import the kit table into your schema file, and never
re-export it, or db:generate creates it a second time.
The kit’s tables
Section titled “The kit’s tables”Rather than adding a column to a kit table, add a table of your own keyed by its id, such as
user_profile (user_id primary key references "user"). It gets its migration from a plain
bun run db:generate, and kit releases never touch it.
When a kit table itself has to change (a Better Auth plugin after bun run auth:schema, an index
of yours on a kit table), bun run db:kit-drift prints the SQL your schema needs that none of your
migrations holds yet. Put it in bun run db:generate --custom --name=<name> and check that it drops
nothing. bun run test fails while any of it is outstanding.
Messages and navigation
Section titled “Messages and navigation”Your messages go in apps/web/messages/en/product.json and apps/web/messages/de/product.json,
keys sorted as Inlang sorts them (_ before digits before letters), named
product_<page>_<element>. The i18n test fails while a key is in one language only.
bun run --cwd apps/web i18n:add product_reports_title --en "Reports" --de "Berichte" puts a
message in its place in both catalogues. A product with many pages can split its messages by
area, product-<area>.json (product-reports.json) in both locales, keys still
product_<page>_<element> (--to product-reports). bun run check, bun run test and vite
add each one to project.inlang/settings.json themselves; commit that file with it, and restart
a running vite dev. Two branches that each add messages to an area of their own then merge
without touching the same file.
A message of the kit’s in your own words goes in product-overrides.json, in both locales, under
the kit’s key. Paraglide reads it last, so it replaces the kit’s text everywhere the kit uses that
key. Keep the kit’s variables: an override may leave one out, never add one, and bun run test
checks both. A release that renames or removes a key you override fails that test too, so you see
it at the upgrade. The kit’s specs look for a message’s text through the message, so your wording
does not fail them.
Plan copy is also in the plan catalogue (@repo/billing/catalog), and i18n.spec.ts fails while
the two disagree.
A refinement in your contract that tags its refusal with params: { reason } gets its copy from
productIssueMessages in apps/web/src/lib/product-forms.ts, keyed by the reason:
title: z.string().refine(notBlank, { error: 'Give the report a title', params: { reason: 'blank_title' },}),
// apps/web/src/lib/product-forms.tsimport type { ProductIssueMessages } from '#lib/forms.js';import { m } from '#lib/paraglide/messages.js';
export const productIssueMessages: ProductIssueMessages = { blank_title: () => m.product_reports_title_blank(),};A link in the sidebar, the mobile menu and the command palette is an entry in productNavLinks,
shown after Home. A page people configure rather than use is a section of the settings dialog
instead (Settings of your own). A link’s label is the message function
itself, which the shell calls as it renders, so it follows the user’s language:
import FileTextIcon from '@lucide/svelte/icons/file-text';import { m } from '#lib/paraglide/messages.js';
export const productNavLinks: ProductLink[] = [ { href: '/app/reports', label: m.product_nav_reports, icon: FileTextIcon, keywords: ['pdf'] },];An action, or an entry that depends on the page, is a command: productCommands(context) in
apps/web/src/lib/product-commands.ts returns groups, which the palette shows after its
navigation. context has the page’s address, parameters and data.
It also names the active organization.
Read what a page has on screen (a selection) from a .svelte.ts module of yours. A label is the
message itself, as for a link, and must not repeat another entry’s text: the palette searches by
it. A command runs once the palette has closed.
import DownloadIcon from '@lucide/svelte/icons/download';import type { ProductCommandContext, ProductCommandGroup } from '#lib/commands.js';import { m } from '#lib/paraglide/messages.js';
export function productCommands({ params }: ProductCommandContext): ProductCommandGroup[] { const report = params.report; if (!report) return []; return [ { heading: m.product_reports_title, commands: [ { id: 'export', label: m.product_reports_export, icon: DownloadIcon, shortcut: 'E', run: () => exportReport(report), }, ], }, ];}A step of your own when someone signs out of this browser, such as revoking a token you issued, is
productOnSignOut() in apps/web/src/lib/product-sign-out.ts
(App shell).
Add the page’s first path segment (reports) to productReservedWorkspaceSlugs, or a workspace
called reports loses its overview: SvelteKit opens your page at that address.
Settings of your own
Section titled “Settings of your own”A page of yours that people configure rather than use, such as what your product connects to or
its defaults, is a section of the settings dialog, not a link in the navigation. List it in
productSettingsSections in apps/web/src/lib/product-settings.ts. The dialog lists your
sections after the kit’s, in a group named after your app, and the command palette lists them
under Settings, found by their label and their keywords:
import PlugIcon from '@lucide/svelte/icons/plug';import { m } from '#lib/paraglide/messages.js';import type { SettingsSection } from '#lib/settings.js';
export const productSettingsSections: SettingsSection[] = [ { href: '/app/integrations', label: m.product_integrations_title, icon: PlugIcon, keywords: ['slack', 'zapier'], pages: { '/(app)/app/integrations': () => import('../routes/(app)/app/integrations/+page.svelte'), }, },];The section stays a route, routes/(app)/app/integrations/+page.svelte with its +page.ts, at
an address of its own, so a link to it works from an email or a bookmark. A link or a goto to it
from any page under /app opens it in the dialog over that page, which stays as it was, with the
data its +page.ts loads, and Back closes the dialog. A visit to the address itself shows the same
dialog over the shell. pages maps each route of the section, by route id, to its page component:
the section’s own, and any page below it that keeps the section marked in the list, such as one
entry’s page below a list of them. hidden: true leaves a section out of both lists, for a page
that is only linked to.
needs shows a section only to the roles that hold it ({ integration: ['read'] }); the page
and the API check again.
The dialog shows the page over another one, so:
- Read route parameters from the page’s
paramsprop, notpage.params, which holds those of the page underneath. - Give the route no
+layout.svelteof its own: over another page the dialog renders the page alone. - Keep its
<svelte:head><title>. Closing the dialog gives the window back the title of the page underneath. - Refresh what a change made here alters on the page underneath, which stays mounted with what it
loaded: a query of it with
queryClient.invalidateQueries, itsloadwithinvalidate(key, true)for a key that load names independs, as the kit’s Security section does for/app’s passkeys. Thetruekeepspage.state, which holds the dialog; without it, SvelteKit resets the state and the dialog closes. Members and Teams invalidate/app’s queries.
A page that draws its own chrome opens the dialog with getShell().openSettings(href)
(App shell). Take the page out of productNavLinks if it was there.
Reserve its first path segment in productReservedWorkspaceSlugs too, as for any page under
/app.
Where a workspace opens
Section titled “Where a workspace opens”A link to a workspace goes to its overview, /app/<slug>: its entry in the sidebar, its card on
the home page, its entry in the command palette, and the page a new workspace opens at. A product
whose workspaces are mostly one page of its own, such as their notes, can open them there instead.
productWorkspaceHome in apps/web/src/lib/product-workspace-pages.ts takes the workspace’s own
path, base, and returns where its links go: one of your workspace tabs
(apps/web/src/lib/product-workspace.ts).
export function productWorkspaceHome(base: string): string { return `${base}/notes`;}
export const productHiddenWorkspaceTabs: readonly KitWorkspaceTab[] = ['jobs'];productHiddenWorkspaceTabs names the kit’s workspace pages your product has no use for,
documents and jobs. They leave the workspace’s tabs and, on a workspace’s page, the command
palette’s shortcuts to them. Their routes stay, so an address someone kept still opens. The
overview and Settings cannot be hidden. The kit’s specs that check where a workspace opens and
which tabs it shows mock this file, so what you choose fails none of them.
The site’s posts and your docs
Section titled “The site’s posts and your docs”The kit wrote one sample into each content folder in 1.1.0: a draft release note
(changelog/first-release.md), a draft post (blog/welcome.md), and a splash page and a getting
started guide in apps/docs/src/content/product. A draft (draft: true) shows in
bun run dev:site and is left out of every build, the feed and the sitemap. Rewrite or delete the
samples: no release adds or changes a file in these folders, and the kit’s own release notes stay
in CHANGELOG.md.
A page you add under apps/docs/src/content/product/guides/ or reference/ appears in the docs
sidebar by itself, so astro.config.mjs needs no edit. Pages are ordered by sidebar.order in
their frontmatter, then by file name (Starlight’s rule), not by title: give each page an order, or
name the files in the order you want. Starlight’s asides (:::note, :::tip, :::caution,
:::danger) work in them. These pages, the kit’s manual, live in apps/docs/src/content/docs,
which bun run dev:manual serves and no deploy publishes.
Versions
Section titled “Versions”The root package.json has two version fields. version is your product’s: the kit ships 0.1.0
and never changes it again, and it is what /api/v1/health, the OpenAPI document and OpenTelemetry
report. kitVersion is the kit release your repository is on, shown on /admin/system and by
bun run doctor; only kit releases change it, so never edit it yourself.
A pre-release of the kit sets it to 1.2.0-next.1 (UPGRADING.md, “Taking a pre-release”).
What still means editing a kit file
Section titled “What still means editing a kit file”A few changes have no product file, because they change how the kit itself behaves. Keep each one small and leave the lines around it as they are, so that a release changing the same file merges cleanly or stops only on your lines:
- Plans, prices, limits and features: the plan catalogue,
packages/billing/src/catalog.ts, which the contract,/pricingand/app/billingare built from. A new limit’s counter and its labels go in your files above. - Better Auth’s configuration: plugins, providers and fields in
packages/auth/src/server.ts. - The self-hosted stack: Caddy’s routes and the Compose files in
infra/. Steps a deploy should run go in the hook folders above, not indeploy.sh.