Configuration
Every key of platformkit.config.js, what it registers, and where the kit reads it.
platformkit.config.js is the one place the kit and the theme learn about a site. It exports a plain object. A change to it restarts the dev server. While it runs, the Vite plugin also watches Markdown edits and refreshes only the build steps whose declared inputs match the changed path.
From the Core checkout, a site command reads this config from the selected site. When that site is a separate checkout, the Git-source CLI installs the kit and theme into its node_modules from the same Core source, without writing the site's manifest or lockfile. Its receipt checks the files npm actually packs for each package, so development-only files in the Core checkout do not cause a false mismatch. A production host can do the same by running npx --yes --package=github:platform-kit/core platformkit deploy from the site checkout; deploying from Core source does not depend on publishing the packages to npm.
export default {
name: 'Northlight Press',
theme: '@platform-kit/movable-types',
modules: { store, print, memberships, sessions, coaching, support, funnels, emails, forms,
instrument, recommendations, translation, tts, enrichment, push }, // true | false
looks: { classic: { css: './src/looks/classic.css', skins: './src/components/landing/skins/index.js' } },
defaultLook: 'classic',
languages: ['en'],
overrides: { SiteMark: './src/components/SiteMark.vue' },
nav: { links: [{ key, href, prefix? }], cta: { href, hrefIn, key, keyIn } },
routes: [{ path: '/tools', component: './src/pages/Tools.vue', prefix?, prerender? }],
app: { tabs: './src/instrument/ui/tabs.js' },
landing: { sections: './src/components/landing/sections.js', schemas: './src/components/landing/schemas.js', stages: './src/components/landing/stages/index.js' },
analytics: { panels: [{ key, component }], events: [{ key, label, goal?, value? }], sources: [{ key, name, hosts }] },
api: { space, public },
instrument: { module: './src/instrument/index.js' },
recommendations: { module: './src/instrument/recommendations.js' },
spaces: { deskSources: './src/community/desk-sources.js' },
admin: { sections: './src/admin/sections.js', models: './build/admin-models.mjs' },
collections: './velite.site.js',
enrichment: { briefs: './build/briefs.mjs' },
data: { schemas: './db/schemas', rows: './db/data.sql', functions: './db/functions', seed: './db/seed.sql', ports: 54320 },
dev: { projectId: 'pk_northlight_local', projectName: 'Northlight Press' },
build: { tests: ['./build/results.test.mjs'], steps: ['./build/steps/books-csv.mjs'] },
}The admin and content store
When a site registers admin.models and has a backend, its production content store must be rows. The build resolves the declared shape into the content-store flag before compiling the browser bundle, so both a repository-backed deployment and a Supabase-only deployment include the CMS. A file-backed build for such a site is refused rather than shipping a production admin without its editors. A backend-free static build remains valid and has no deployed CMS or authentication.
The keys
| Key | What it is |
|---|---|
name |
The site's name: the header, the admin's title, the id of its local stack |
dev |
Local development identity: projectId is the stable Supabase project id (default pk_<site>_local); optional projectName labels the site in the workspace directory without changing SEO metadata |
theme |
The theme package. Its manifest (src/theme.js) names the entry, the design stylesheet, the strings directory, the section schemas and the phone probe's allow-list |
deployment |
Which of the three shapes this site is: { backend, content, repo }. Every field defaults to what a site already did, so a site that says nothing is unchanged. backend: false is a static site — no Supabase at all — and is declared rather than inferred from a missing key, so that switching on a module needing one fails at the commit instead of at a deploy. One function, deploymentShape(), resolves the block against the environment and the administrator's settings and is the only place the shape is answered. See The three deployment shapes |
modules |
The switches. Every module is on unless false. See Modules |
looks |
The palettes by name: each a css file and, optionally, a skins module (sections rebuilt for that look). Every look's CSS is imported; content/site.md picks the one worn |
defaultLook |
The look when site.md names none |
languages |
The catalogs under content/strings/<lang>/ |
overrides |
Theme components replaced by name: { ComponentName: './path.vue' }. Every import that lands on the theme's file gets the site's, however written; the override importing the component it replaces gets the original |
nav |
The header: links (each a strings key under nav, an href, and prefix: true to stay lit for paths under it) and the cta (its href/key, and hrefIn/keyIn for a signed-in person). Absent: the theme's default |
routes |
The site's own pages: a path, a component, prefix for paths under it, prerender: false to leave one out of the build's pages |
app.tabs |
A module exporting the /app shell's tabs (component, words, start(), props(), first) and answered() |
community.places |
Places of the site's own in the community's menu (the social layout): each a key, a word (a key of ## rail in the site's spaces.md), an icon, its page's component, wide, shows (signed-in or everyone) and claims — old addresses that now forward to it (!/x keeps one out). Its page is /feed/<key> and everything under it |
community.layout / community.path |
chat presents the space workspace; social presents one network-style home at the configured path. In the social layout, bare /spaces resolves to that home; individual /spaces/<slug> routes remain separate |
community.access |
Admission rules for Events, Messages and Notifications; Home's rules are under network.access |
network.access |
read and write admission rules, a join offer slug, and an optional plans path to alternative plans |
coaching.requireAuthToBook |
Sign-in is required by default. Set false to expose booking to guests, subject to each session's requires rule. Free bookings ask for an email; paid bookings carry the selected time through Stripe Checkout and use Stripe's returned email for fulfillment |
landing |
The site's own landing section types: a sections module (SECTIONS, components by name), a Node-safe schemas module (SECTION_SCHEMAS: group, sketch, label, description, summary, defaults, a zod schema, field config — and STAGE_SCHEMAS, the same for the hero's stages), and a stages module (STAGES: the components the hero's visual may be, by key) |
analytics.panels |
Panels added to the dashboard: { key, component } |
analytics.events |
The site's own events, shown by their labels on the dashboard: { key, label, goal?, value? } — a goal joins the Overview's goals and the channel columns, value names the property summed as the event's value |
api |
The content API's settings: space (the id Contentful clients name; the site's slug by default) and public (Delivery readable with no key; true by default). See the content API |
analytics.sources |
Other sites reporting in through the analytics script: { key, name, hosts } — the key a page sends, the name on the dashboard's Source select, the hosts allowed to send under it (*, *.example). Written for the ingest function by the analytics-config step; the browser gets names only (see the analytics script) |
instrument.module |
The component /app mounts and the vocabulary the admin names a reading by (TYPEN, DOMAINS, LEVELS, STATEMENTS, …). A contract: every export has a default when absent |
spaces.member |
A module exporting panels: what this site puts on the private half of a member's card in a space. Each is { key, title, component }, and a panel fetches its own data behind the database's own rule. A contract: no panels when absent |
spaces.deskSources |
A module exporting sources, the optional staff Desk adapters. Empty by default; a source appears only when registered and its module is on. Social conversations are opt-in here, and Social Admin remains the fallback when they are not registered. See Spaces |
recommendations.module |
readerProfile, readerSignals, recommendedBooks, topicByKey. A contract with defaults |
admin.sections |
A module exporting sections: { sec, after?, key, label, icon, module?, title, lead, component } — a view added to the admin's rail under its group (or a new group after the one it names) |
admin.models |
A module exporting models — the site's own records in the admin's one registry, each a grammar, an editor, checks, actions and a hook where it needs them (The admin as a host) |
collections |
A module exporting the site's own velite collections (and optionally prepare) |
enrichment.briefs |
The module exporting the enrichment's six stage prompts, over the site's own voice |
data |
The site's own data layer: schemas (declared schema files), rows (an idempotent data script), functions, seed, and ports — the base of the local stack's port range (+1 the API, +2 the database, …). data.migrations is refused: there are no migrations |
cloud |
Where cloud mode publishes: { repo: 'owner/name', branch: 'main' }. A declaration, so a fresh deployment already knows its target and nobody has to type one into the admin — where it is set, the admin shows it and will not change it. The token is the one secret and stays in app_settings (Content → Publishing); a site that declares no repo is told its target in the admin as before. repo: false declares the repository out of the cycle: nothing is pushed, the export only writes rows out to files, and Publishing asks for none — see Cloud mode. It is declared rather than inferred from whether a token is set, because the case that has to be safe is a token that was set. Written for the functions by the cloud-config step |
webhooks |
The subscriptions this deployment ships already wired: a list of { kind, url, … } — content (name, topics), form (form, tag, secret), scheduling (triggers, secret), support (subscriptions). url must be https. A secret names an environment variable (secret: 'env:FORM_HOOK_SECRET') and a bare value is refused, so none can reach the repository. Written by the webhooks-config step and pushed into the four subscription tables at deploy (source: 'file'); a subscription somebody makes through the API is theirs (source: 'hand') and is never touched. See Declarations and state |
build.tests |
Scripts platformkit test runs after the kit's own |
build.steps |
The site's own build steps, a module exporting one record each, run in their phase among the kit's (the build steps) |
Paths are relative to the site's root. Every module named is loaded with import() and must be importable under Node as well as under Vite where the kit reads it at build time (the schemas, the collections, the handlers, the briefs); the components are Vite's alone. In development, Vite also answers GET /__platformkit/site-meta for workspace directory previews, using the built site/page/UI records and the shared SEO resolver.
A shop of your own
store.suppliers and store.kinds are how a site adds to the shop rather than configuring
it. A supplier is whoever makes or sends a thing — the kit ships three printers, the
shop’s own post room and a download; a site may add a bindery down the road, a dropshipper,
anything that can answer the interface. A kind is a sort of thing to sell whose needs are
its own, reaching the store through two slots and no more.
store: { suppliers: './build/suppliers.mjs' },The registry walks the kit’s and the site’s as one list, and nothing downstream knows which a
supplier came from — which is the only way a seam is real. One that does not keep the shape is
refused by name when it is registered, with what is wrong with it, rather than halfway through
somebody buying something. Each declares the environment keys it needs, so a deploy can
refuse before it starts: a sale that fails at the till is finding out too late.
Two libraries vite must leave alone
The plugin excludes onnxruntime-web and @matrix-org/matrix-sdk-crypto-wasm from vite's dependency
pre-bundling, and both for the same reason: each finds its own runtime files at run time, relative to
its own module. Pre-bundled, the Matrix crypto library's .wasm resolves to a path under
node_modules/.vite/deps that the dev server answers with HTML, and WebAssembly.instantiateStreaming
refuses it as the wrong MIME type — with nothing else to show that anything is wrong. Left as it is,
the same address is the real file and arrives as application/wasm. Importing the file directly
instead is refused: that package's exports map has a single entry.
Reading the config in code
The kit loads it with loadSite() (packages/kit/build/site.mjs), which fills defaults, resolves paths, and resolves the module manifests — so site.moduleKeys is every module that exists and a name outside it is refused rather than read as "on". In the browser the config's public part is #site; each registration is its own virtual module (#site/nav, #site/landing, #site/app…) — see The theme and overrides.