Skip to main content
All apps
Sovrium.com
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.

app.ts
/**
 * 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 app

Run it

Install Sovrium, then start the app from its config.

>_ terminal
curl -fsSL https://sovrium.com/install | sh
git clone https://github.com/sovrium/website.git
cd website
sovrium start app.ts

Build your own

Start from a template or an empty file. One config, and Sovrium runs the whole app.

Built with Sovrium