
WebsiteReference app
Sovrium.com
The marketing site, documentation, and comparison library you are reading right now, in one Sovrium config: 40+ pages in two languages, a docs zone built from markdown files, public forms, tables, and analytics. Published read-only as a worked example of a production configuration.
This is the configuration that runs sovrium.com, published read-only. Read it, learn from it, then start your own from a template.
The configuration
The real configuration that runs it, verbatim. The full source is on GitHub.
/**
* Copyright (c) 2025-2026 ESSENTIAL SERVICES
*
* This source code is licensed under the Business Source License 1.1
* found in the LICENSE.md file in the root directory of this source tree.
*/
// Sovrium marketing website — entry point.
//
// Config is split into per-entity sub-modules under `config/`, each authored
// with a type-only `@sovrium/types` import and `satisfies`-checked against its
// sub-type (`PageConfig`, `AuthConfig`, `LanguageConfig`). This file aggregates
// them into a root `AppConfig`-typed object, so TypeScript verifies the full
// shape at author time with no cast. `sovrium validate app.ts`
// (Effect Schema decode) remains the authoritative runtime source of truth.
//
// This tree is PUBLISHED: it is mirrored verbatim — flattened to the repo root —
// as github.com/sovrium/website. So reference sibling files app-root-relative
// (`config/pages/docs/`), never by their path inside this monorepo, and never
// point at monorepo-only trees. Both would be dead links for a public reader, and
// `scripts/build/publish-website-repo.ts` fail-closes the release on the first
// one it finds — a leaked internal path aborts the publish, it does not warn.
//
// Type-only import (no value import of the types): the standalone sovrium binary
// loads this config with bare-package imports left unresolved, so only type-only
// imports — erased at runtime — are safe.
import type { AppConfig } from '@sovrium/types'
import languages from './config/languages'
import auth from './config/auth'
import { components } from './config/components'
import home from './config/pages/home'
import productsPartner from './config/pages/partner'
// Cloud pre-launch waitlist landing (`/{lang}/cloud`, no `/products/` segment).
// Per-locale marketing landings for the managed-hosting offering: hero email
// capture → qualification survey. All external cloud.sovrium.com waitlist CTAs
// (nav, homepage cloud band, install) now funnel here. Backed by the
// cloud_waitlist + cloud_survey tables and the single bilingual cloud-waitlist +
// cloud-survey forms below.
import { cloudPages } from './config/pages/cloud'
// Academy pre-launch waitlist landing (`/{lang}/academy`, no `/products/`
// segment). Per-locale marketing landings for the planned training offering:
// hero email capture → qualification survey. Backed by the academy_waitlist +
// academy_survey tables and the single bilingual academy-waitlist +
// academy-survey forms below.
import { academyPages } from './config/pages/academy'
// Sovrium — The Journey pre-launch subscribe landing (`/{lang}/journey`). The
// secondary CTA of the Cloud + Academy waitlists and the closing invitation of
// the About page: a build-in-public vlog/newsletter subscription. Backed by the
// journey_subscribers table + the single bilingual journey form below.
import { journeyPages } from './config/pages/journey'
import { waitlistPages } from './config/pages/waitlist'
import termsOfService from './config/pages/terms-of-service'
import privacyPolicy from './config/pages/privacy-policy'
import dataDeletion from './config/pages/data-deletion'
// The company's own account, in two halves. `/manifesto` is the conviction —
// the diagnosis, the specification, the product, the model, the horizon — and is
// the first link of the navbar: what Sovrium believes comes before what it does.
// `/our-story` is the narrative behind it, and the manifesto links into it twice.
//
// These two were ONE page (`/about`) from 2026-07-25 until 2026-08-01, when the
// founder's rewrite re-split them: a manifesto and a founder's story do
// different work and are read by different people at different moments, and
// folding them together had cost each of them its shape. `/about` 301s to
// `/manifesto` (see `redirects[]` below).
import manifesto from './config/pages/manifesto'
import ourStory from './config/pages/our-story'
import security from './config/pages/security'
// Branded custom 404 (served by the platform at path /404 for any unmatched
// route) — replaces the bare platform default with full chrome + helpful exits.
import notFoundPage from './config/pages/not-found'
import { changelogPages } from './config/pages/changelog'
// Sovrium Academy pre-launch waitlist funnel: a hero email capture
// (academy-waitlist → academy_waitlist table) redirecting to a standalone
// qualification survey (academy-survey → academy_survey table), using the
// anonymous public-form submission mechanic (access: { require: 'all' }).
// Sovrium — The Journey subscribe capture: an anonymous public email capture
// (journey → journey_subscribers table) ending on an inline success page (no
// survey funnel), using the anonymous public-form mechanic (access:
// { require: 'all' }).
import waitlistTable from './config/tables/waitlist'
import journeySubscribersTable from './config/tables/journey-subscribers'
import { waitlist } from './config/forms/waitlist'
import { journey } from './config/forms/journey'
// Documentation zone (absorbed from the retired apps/docs): a landing at
// `/{lang}/docs` + a markdown contentDir fanning out `/{lang}/docs/:slug` over
// every article under content/docs/{en,fr} (~200 per locale, kept at exact
// EN/FR parity). See config/pages/docs/.
import { docsPages } from './config/pages/docs'
// Sovrium Apps community gallery: a dedicated `/apps` searchable/filterable card
// grid + `/apps/:slug` detail pages, generated statically from a real-app catalog
// (config/pages/apps/). Working configs are the proof — show, don't tell.
import { appsPages } from './config/pages/apps'
// "Submit your app" — a ledger-only public form opened from a modal on `/apps`
// so anyone can propose their Sovrium app for review before it joins the gallery.
import { submitApp } from './config/forms/submit-app'
// Public-form notifications — one per public form, so a submission reaches a
// person instead of only sitting in a table row.
import notifyOnWaitlistSignup from './config/automations/notify-on-waitlist-signup'
import notifyOnJourneySignup from './config/automations/notify-on-journey-signup'
import notifyOnAppSubmission from './config/automations/notify-on-app-submission'
const app: AppConfig = {
name: 'sovrium-website',
languages,
auth,
// Shared nav/footer chrome (site-header, site-footer) referenced
// by every page via the `{ component: 'site-header' }` / `{ component:
// 'site-footer' }` shorthand — DRYs out ~120 lines of previously-inlined markup
// per page and makes the conversion CTA editable in one place.
components,
// Search is a DOCS-zone affordance since the navbar simplification: only the
// docs pages inline `searchScript` (they set it explicitly) and only the
// `docs-header` mounts the ⌘K modal — marketing pages ship no search runtime.
// ── Retired URLs ──────────────────────────────────────────────────────────
// Every merged or deleted page answers with a 301 rather than a 404, so
// bookmarks and backlinks survive and search engines consolidate the old
// URL's ranking onto the new one. The engine re-applies the active locale
// prefix to both sides, so `/fr/products/platform` lands on `/fr/`.
//
// `/install` is deliberately ABSENT from this list. The page lived only at
// the locale-prefixed `/{lang}/install`; the bare `/install` path is served
// by the 292-line shell script in `public/`, which is the product's primary
// install path (`curl -fsSL https://sovrium.com/install | sh`). A redirect
// rule here would shadow it.
//
// `/admin-analytics` gets no redirect either: it was a hand-rolled analytics
// page duplicating the platform's own auto-mounted `/_admin` console, and
// that console 404s anonymous callers by design (S1 anti-enumeration).
// Redirecting a public URL at it would advertise an admin surface.
//
// `/login` DOES get a redirect, and its `localizeTarget: false` is the whole
// reason it can. `/_admin/login` is the console's one public carve-out — it
// serves 200 while every other `/_admin/*` route 404s an anonymous caller —
// and it lives OUTSIDE the locale namespace. Without the flag a root-relative
// `to` inherits the request's prefix, so `/en/login` would resolve to
// `/en/_admin/login`: a 301 into a 404, which is strictly worse than a clean
// 404 because it burns the redirect, still fails, and walks a crawler to a
// dead end. That is why this rule was ABSENT from 2026-07-25 to 2026-07-26,
// filed as a platform gap. `localizeTarget` (DEC-069) closed it.
//
// Deleting the flag therefore does not merely change a URL — it re-breaks the
// rule. If a future pass wants the target localized, the target has to move
// into the locale namespace first.
redirects: [
// The platform page's H1 was a synonym of the home H1 and its six value
// cards were `home.features` with different nouns; its real content (field
// types, components, RBAC) is reference material that lives in /docs.
{ from: '/products/platform', to: '/', status: 301 },
// With platform gone, `/products/` was a taxonomy for a set of one.
{ from: '/products/partner', to: '/partner', status: 301 },
// Emits `/_admin/login` verbatim from `/login`, `/en/login` and `/fr/login`.
{ from: '/login', to: '/_admin/login', localizeTarget: false },
// `/about` was itself the survivor of an earlier merge — it absorbed
// `/manifest` in the 2026-07-25 restructure. The 2026-08-01 split undoes
// that: the conviction half reclaims a name of its own. The manifesto is the
// right target of the two, because it is what an `/about` visitor is
// actually looking for — who these people are and what they believe — and it
// links into `/our-story` from inside its own prose.
//
// Locale-prefixed by the engine on BOTH sides (no `localizeTarget: false`
// here, unlike `/login` above): both paths live inside the locale namespace,
// so `/fr/about` lands on `/fr/manifesto` rather than on the EN page.
{ from: '/about', to: '/manifesto', status: 301 },
// The 2026-Q3 docs restructure moved every article into one of eleven zone
// tabs — but tab membership is `section:` FRONTMATTER and the slug is the
// filename, so re-organising the whole corpus cost no URLs. This is the one
// deliberate rename: `/docs/overview` became meaningless once every tab had
// an overview page, and the article was already titled "Schema Overview".
//
// Never add a docs rule whose `from` matches a slug that still exists under
// content/docs/{en,fr}. Redirects resolve BEFORE page routing, and the
// collision check covers only STATIC page paths — the docs route is dynamic
// (`/{lang}/docs/:slug`), so a shadowing rule would make a live article
// unreachable with no boot error and no validation failure.
{ from: '/docs/overview', to: '/docs/schema-overview', status: 301 },
// The `sovrium` npm package is retired, and it never resolved anyway: the
// root package.json is `private: true` with no `main`/`exports`/`bin`, so a
// bare-specifier import of it had no entry point to reach. Four articles
// documented it as a library API. They are deleted, and the binary is now
// the only documented way to run Sovrium.
//
// Split by intent: the runtime pages land on the CLI, which is what a reader
// looking for `start()` or `build()` actually needs. The type reference
// lands on the authoring page for `@sovrium/types`, a DIFFERENT package that
// is genuinely published and untouched by this removal.
//
// Do not quote the retired import line anywhere in this tree. A guard grep
// hunts it, and a comment carrying the very string it looks for would make
// that guard fire forever on itself.
{ from: '/docs/typescript', to: '/docs/cli', status: 301 },
{ from: '/docs/typescript-start', to: '/docs/cli', status: 301 },
{ from: '/docs/typescript-build', to: '/docs/cli', status: 301 },
{ from: '/docs/typescript-types', to: '/docs/configuration-typescript', status: 301 },
// Pure CLI content all along; the `typescript-` prefix was a remnant of the
// family above. Renamed to `cli-validate`, so this one is a rename, not a
// removal.
{ from: '/docs/typescript-validate', to: '/docs/cli-validate', status: 301 },
],
pages: [
home,
manifesto,
ourStory,
productsPartner,
...academyPages,
...cloudPages,
...journeyPages,
...waitlistPages,
termsOfService,
privacyPolicy,
dataDeletion,
security,
notFoundPage,
...changelogPages,
...docsPages,
...appsPages,
],
// The Academy + Cloud pre-launch waitlist funnels: an email-capture table plus a
// qualification-survey table each. The schema initializer provisions them
// additively on boot and the auto-mounted admin data console surfaces both the
// tables and their form-submission ledgers (the site has `auth`, so the admin
// surface is active).
tables: [waitlistTable, journeySubscribersTable],
forms: [waitlist, submitApp, journey],
// One notification per public form. All three use a `form` trigger rather than
// a `record` one: two of the three write to a table that other paths also
// write to, and the third (`submit-app`) declares no table at all, so `form`
// is both the narrower and the only universally-available choice.
automations: [notifyOnWaitlistSignup, notifyOnJourneySignup, notifyOnAppSubmission],
// Declared environment. The site had no `env` block until these notifications
// needed a recipient — and a `$env.` token only resolves for a DECLARED key,
// so an automation referencing an undeclared variable silently receives the
// empty string rather than an error. Declaring it also opts the value into
// automatic redaction from stored automation run history.
env: [
{
key: 'LEAD_NOTIFICATION_EMAIL',
required: false,
description:
'Recipient for the public-form notifications (waitlist, newsletter, gallery submission). Deployment configuration rather than product configuration, so it is referenced as `$env.LEAD_NOTIFICATION_EMAIL` and never written into the config. Optional so local previews and E2E boot without it; unset means the notification send fails while the submission itself still succeeds.',
},
],
analytics: true,
// Auto-generated /llms.txt + /llms-full.txt (llmstxt.org convention). The
// routes auto-derive from the docs contentDir pages above; we override the H1
// title + blockquote description and keep `full: true` so /llms-full.txt
// concatenates every article body. Docs-as-AI-corpus is a first-class goal of
// the unified site.
llms: {
title: 'Sovrium',
description:
'Sovrium is a source-available, self-hosted platform that turns a single configuration file into a complete web application. This is the full documentation corpus.',
full: true,
},
// The site ships its OWN site-wide Cmd+K search modal (config/pages/docs/
// search-modal.ts, mounted in site-header). Opt out of the platform-synthesized
// command palette so the two overlays don't both open on the same keystroke.
palette: { enabled: false },
// ADR-024 made the platform default achromatic: colour is reserved for
// consequence. Amendment A3 then let each app declare its OWN accent so the
// engine's decorative token could be deleted without shipping a half-designed
// public site — this app declared a terracotta one here.
//
// A5 retires that licence for this site. A3 was a sequencing device, not a
// permanent grant: it bought the time to redesign the marketing site properly
// rather than neutralise it half-finished. That time has been spent, so the
// accent block is deleted and every surface that referenced it now resolves
// against the platform's neutral roles.
//
// `theme` deliberately stays DEFINED rather than being removed outright. It
// now carries the document rhythm instead of a palette.
theme: {
// ── The line ──────────────────────────────────────────────────────────────
// One line L = 28px = 1.75rem. Body text sits on it exactly (17px/28px), so
// every vertical gap in the document is a whole number of lines the reader
// can count. Four rungs, not a geometric ramp: linear multiples read as
// structure, a ramp reads as decoration.
//
// Keys are kebab-case with NO DIGITS — the schema's key pattern is
// /^[a-z]+(-[a-z]+)*$/, so `line-two` is legal and `line-2` is not.
//
// These govern DOCUMENT rhythm only. Component-internal padding (a button's
// px, a chip's py) stays on the platform's 4px scale; a control is not a
// paragraph and forcing it onto a 28px grid would just make it wrong.
spacing: {
line: '1.75rem', // 28px — 1L
'line-two': '3.5rem', // 56px — 2L
'line-three': '5.25rem', // 84px — 3L
'line-four': '7rem', // 112px — 4L
},
// Follow the visitor's OS preference before content renders. This was not
// affordable while the site carried a terracotta accent — an accent has to
// be re-tuned for a dark ground, and getting it wrong is worse than not
// offering dark mode at all. With the palette achromatic the dark side is
// the same ramp inverted, which the platform token layer already ships, so
// honouring the preference costs one line.
//
// A stored visitor choice (`localStorage.theme`, set by the header's
// theme-toggle) still wins over this default once the no-FOUC head script
// runs. This sets the starting point, not the policy.
colorScheme: 'system',
},
// The site follows the visitor's OS preference (theme.colorScheme above).
// The `theme-toggle` in site-header lets them override it explicitly,
// persisted via the platform's auto-injected [data-theme-toggle] runtime.
}
export default appRun it
Install Sovrium, then start the app from its config.
curl -fsSL https://sovrium.com/install | sh
git clone https://github.com/sovrium/website.git
cd website
sovrium start app.tsBuild your own
Start from a template or an empty file. One config, and Sovrium runs the whole app.