Phase 3 Additions: System Design Handover
| As of | |
|---|---|
| Version | 1.0.0 |
| Kind | plan |
| Length | 2,661 words, about 12 min |
| Tags | system-designhandoverone-repoi18nsynergy |
Retired 2026-11-06. The pass settled all twelve questions with Pratik and wrote the answers into the live pages (D249 to D260): Public site, Hosting and domains, Design tokens, Native UI, Internationalization, the Synergy overview, Runtime validations and Dependency flow. Two answers changed the scope: the public site is temporary (D256), and Mantine leaves the App in a new lesson M1 (D255). This page stays as the record of the questions.
The system design was last written for Synergy alone: Synergy 1.3.0, from arkive #25, with the violations work (#33, #34) folded in since. Phase 3 has grown after that. Lesson 0 merges Arkive into Sidekick, the site lessons S1 to S7 rebuild the public site and move it to ossidekick.com, and the i18n lessons I1 and I2 replace @sidekick/copy (arkive #38 to #50). Those decisions live in PRD, spec and plan pages, and the system design pages they touch still describe the old shape.
This page is the input for one system-design pass. It lists what was added, where the current design is now wrong or silent, and the questions the pass has to settle with Pratik, each with a recommendation. The pass writes its answers into the system design pages; this page then retires.
Read first
- One Repo (D213 to D227) and the spec's One Repo section (stories 95 to 106, D228 to D239).
- Internationalization (D240 to D248). It is already a system design page, so the pass connects it to the rest rather than redesigning it.
- The Synergy plan 2.3.0: the i18n lessons, the site lessons and "Decisions made while planning".
- Lesson 0, for the order in which the repo changes land.
- The pages the pass edits: Dependency flow, eslint-plugin-boundaries, Vercel, Technology stack, CSS modules, Prettier at repo level, Shared tsconfig and the Synergy overview.
- The design handover for the new site, in the project's shared files (
one-repo/design/). It is never committed.
What was added
| Addition | Datums | Built in |
|---|---|---|
One repo: content/docs, content/writings, apps/www, apps/app, @sidekick/content, licences | D213 to D217 | Lesson 0, PRs 1 to 3 |
The site type-checked with astro check and hosted on the sidekick-www Worker with branch previews | D220, D231, D233 | Lesson 0, PRs 4 and 5, hosting |
| No published URL changes, tested by the sitemap diff | D221, D234 | Every step |
Project vocabulary in 07-glossary/, no CONTEXT.md | D226 | Lesson 0, PR 1 |
English only but i18n-ready: next-intl, use-intl, colocated catalogs, @sidekick/i18n, logical CSS, lint | D240 to D248 | I1, I2 |
The new site: one 58rem breakpoint, milestone tiles, Writings-only feed, components in content | D223 to D225, D227 | S1 to S6 |
The cutover: ossidekick.com, app.ossidekick.com, arkive.blog 301s for a year, no App link | D218, D219, D222, D235 | S7 |
Order inside Phase 3: Lesson 0, lessons 1 to 13, Checkpoint 3a, I1, I2, S1 to S7, then 3b.
Where the design is now wrong or silent
Grouped by area. Each item names the page, what it says today and what has been decided since. Items marked (decide) need a question below; the rest are updates the pass can write directly.
Repo and package map
- Dependency flow (1.6.0). The apps row names
apps/weband "laterapps/cli"; there is noapps/wwwand nocontent/. Foundation listscopy, which I1 deletes, and noti18n, which I1 creates.dependency-flow.svgshows the same. (decide: Q1, Q2, Q3) - React as peer dependency (D58) says
apps/webowns the installed React. Two apps now install it:apps/app, andapps/www, which renders content components withrenderToStaticMarkup(D227). Both takereactfrom the pnpm catalog; the page should say so. - eslint-plugin-boundaries.
apps/*is oneappelement, soapps/wwwwould get an app's rights: the registry, features, platform packages and@sidekick/core, which holds the database client.content/matches no element. (decide: Q1, Q2) - Synergy overview. The package table and
synergy-packages.svgnameapps/web; D201 says every label comes frompackages/copy. (decide: Q4, Q5) - Lesson 1's move list still moves
copyintopackages/foundation/, and I1 deletes it a few weeks later. That is fine, but the pass should say it is expected, so nobody "fixes" lesson 1 to skip the move.
Hosting and domains
- Vercel (D54): root directory
apps/web, and the stablevercel.appURL asNEXT_PUBLIC_APP_URL. Since then: root directoryapps/app(Lesson 0, PR 2), Skip deployment on for content-only commits (D215), and from the cutoverapp.ossidekick.comas a DNS-only CNAME, withNEXT_PUBLIC_APP_URLand Supabase's Site URL changed on the same day (D218). - No page describes the public site's hosting. The Worker (D220), the URL rule (D221), the zero-downtime cutover and the arkive.blog redirect rules (D222) live only in the PRD and spec. (decide: Q9)
- Technology stack (1.0.0) has one hosting row (Vercel) and no row for the public site (Astro, Cloudflare Workers) or i18n (next-intl, use-intl).
- System overview diagram (
architectural-overview.svg) shows only the App's tiers. The public site is a second, static surface with no API calls. - Turbo env. The shared
buildtask lists the App's environment variables (NEXT_PUBLIC_APP_URL, Supabase, Stripe), so a change to any of them also misses the site's cache. Small, but@sidekick/www#buildcan declare its ownenvwhen it gets itsinputsline in Lesson 0. - Node version files. Sidekick's CI reads
.nvmrc(24); the spec gives the site.node-version24 because Cloudflare reads that file. Two files for one version should be stated as deliberate, or one should go.
Code quality and tooling
- Prettier (D56):
.prettierignoregainscontent/(D216) and the files the site ships as given, and the root gainsprettier-plugin-astro. The page's script examples also predateprettier --check .. - ESLint does not reach the site yet.
apps/wwwhas nolintscript, and nothing decides how.astrofiles are linted. When it does run, the Pages Router ban ineslint.config.jsmatchesapps/*/src/pages/**with selectorProgram, so it fails every Astro page. D248'sno-literal-stringmust cover the site's user-facing code. (decide: Q8) - Stylelint.
lint:stylesglobs only*.module.css, andselector-class-patternrequires camelCase. The site's stylesheet is global with kebab-case classes (.prose,.wide), andstylelint-use-logical(D246, D248) has to see it andtokens.css. (decide: Q7) - Shared tsconfig (D50) says every package extends
tsconfig.base.json; the site extendsastro/tsconfigs/strict. (decide: Q8) - CI. The labeler workflow (D215) is the first one that needs
pull-requests: write; CI is read-only today.pnpm typecheckgainsastro checkand the build gains the privacy and datum gates (D231). The catalog check (D248) becomes a Turbo task. Testing layers should say which of these are checks and which are tests.
Styling and the design system
- One token source. D227 moves
tokens.cssinto@sidekick/uias "the one token source". The App styles with Mantine's theme and CSS modules (D47, the Mantine page), and nothing says whether the Mantine theme reads the same variables. (decide: Q6) - CSS modules everywhere (D47) says "all styling uses CSS modules without exception". The site's S1 stylesheet is global, and only its content components use CSS modules. (decide: Q7)
- The one breakpoint (D223) is written as a site rule. The App uses Mantine's breakpoints. The pass should say that D223 binds the site only, or extend it to the App on purpose.
The public site
- The site has no system design page. Its pipeline (
vault.mjs,entries.ts,tree.ts, the remark and rehype plugins), thepublish: truegate, the privacy and datum gates, retired entries and "As of" are written only in the site's agent notes (CLAUDE.md, becomingapps/www/AGENTS.md). (decide: Q9) - Components in content (D227): a remark plugin loads a component by name, validates its props, renders it with
renderToStaticMarkupand replaces the fenced block. Two things are unsettled. Where the name-to-component map and the props schemas live (zod is the house validator, D49). And whether a CSS module imported by a component rendered from inside a remark plugin is processed and emitted by Astro's build, since that component is outside Astro's component graph. (decide: Q10) - One interface (D48) forbids pages that read the database directly. The site reads Markdown files at build time, which is not user data, so it does not break D48, but no page says so. It matters later: datums move into the database at Taxila beta, and then the site's datum index would read through the API like any other consumer.
- Live paths. Violations and exceptions cites the arkive repo's
CLAUDE.mdandsrc/lib/datums.mjs, which becomeapps/www/AGENTS.mdandapps/www/src/lib/datums.mjs. Lesson 0's step 9 does not list this page.
Internationalization against the rest of the design
@sidekick/i18n's layer. It is domain-free, so foundation. But foundation packages never import each other, anduiprimitives have words of their own (a close button's accessible name). (decide: Q3)- Whose namespace a type label is in. D201 keys a type's label as
synergy.type.sandbox-notes.note. D243 says a package's strings live in its own catalog under its own namespace, so the label "Note" belongs to thesandbox-notesprovider, not to Synergy. (decide: Q4) - How the Sneak Peek Link gets its words. The component is host-agnostic: the host injects the transport, and a third party can render it. A
useTranslationscall needs a provider above it, which Sidekick's pages get from next-intl and a third party does not have. (decide: Q5) - Validation messages.
/contractsshares zod schemas between server and browser (D199). Zod's default messages are English sentences, which breaks keys-never-copy (D201) once they reach an API response. (decide: Q11) - The person's locale and time zone. D242 reads the locale from "their setting" and D245 shows dates "in the person's time zone", but the profile has neither field. next-intl also needs an explicit
timeZone, or server-rendered and browser-rendered dates can differ. (decide: Q12) - Amounts with a currency (D245) are a rule for every future table, so it belongs in the database conventions (syncable tables) as well as on the i18n page.
- Other consumers of keys. The API returns keys, so
apps/cli, bots and third parties need the catalogs to show labels. Nothing in Phase 3 needs it; the pass should record it as a known gap, not design it. - Retiring Centralized Copy. I1's docs PR retires D36, rewrites D23 in best practices and fixes the architecture invariants (plan). The pass only confirms that list is complete.
Security and auth
- The App's sign-up is off and the site has no App link until Open House (D219). The authentication page should record the sign-up setting and the Supabase redirect URLs that change at the cutover (Site URL, the
vercel.appURL, localhost and the preview pattern kept).
Questions for the pass
One at a time, recommendation first, Pratik decides. Write each answer into the page it changes, with a Decision History row and a log row.
- What may
apps/wwwimport? Recommended: a new boundaries element type,site(apps/www), that may import only foundation packages, and that nothing imports. The public site is static and public; it must never reachcore's database client, the registry or a feature, and lint should make that impossible rather than unlikely. Runner-up: keep it anappand rely on review. - Is
content/an element? Recommended: no. It holds Markdown and no code, so ESLint ignores it, and the site reads it throughvault.mjsby path, not by import. The@sidekick/contentdependency exists for Turbo's hashing and Vercel's skip (D215); the dependency flow page should say exactly that, so nobody adds code to it. - Where does
@sidekick/i18nlive, and mayuiuse it? Recommended:packages/foundation/i18n, anduiprimitives take their words as props, so the foundation rule stays flat anduistays usable in a host that has no catalogs. Runner-up: one explicit exception,uimay importi18n, written as a boundaries rule. - Who owns a type's label? Recommended: the provider. Each provider's catalog holds its types' labels (
sandbox-notes.type.note); Synergy's catalog holds only Synergy's own words (link states, the seeded relation types, error codes). This follows D243 and keeps "deleting a feature deletes its words". D201 is rewritten to say so, and itssynergy.type.<provider>.<type>example goes. - How do the Sneak Peek Link's labels reach it? Recommended: the component calls
useTranslations('synergy')fromuse-intl, notnext-intl, and the host wraps it in an intl provider. In the App that is next-intl's provider, which is use-intl's; a third party wraps it in use-intl'sIntlProviderwith Sidekick's catalog. The pass should check that one copy ofuse-intlserves both, the way D58 does for React. Runner-up: labels injected as a prop, like the transport. - Is
tokens.cssthe App's token source too? Recommended: yes, as the target, and not built yet.tokens.cssis the one source for the site and foruifrom S1; the App's Mantine theme reads the same CSS variables when the App is next restyled, and until then the page says the App is not on the tokens. A second source is what D227 set out to prevent. - What does D47 cover? Recommended: CSS modules for every component, in the App, in packages and in the site's content components. The site may also have one global stylesheet for its chrome and prose. Stylelint gets a second config block for the site: kebab-case classes, and
stylelint-use-logicalon every CSS file in both apps andui. - How do the site's TypeScript and lint join the repo's? Recommended: the site keeps
astro/tsconfigs/strictand adds the base options Astro does not set, and D50 names the site as its one exception. In I2,apps/wwwgets alintscript witheslint-plugin-astro, and the Pages Router ban is narrowed to the App's folders. - Where does the site's design live? Recommended: two new pages. A Public site page in
functional-design/(the content pipeline, gates, routes, retired entries, milestones, the feed, components, the JS budget), withapps/www/AGENTS.mdkeeping only the short rules. And a Hosting and domains page innon-functional/that holds the App on Vercel (D54, revised in place), the site on the Worker (D220), the domains and the cutover (D218, D222), with the Vercel page folded into it. - How are content components built and styled? Recommended: a component map in
apps/www(name, component, zod props schema), the way the feature registry collects manifests, with a build failure on an unknown name or bad props (D238). Before the design commits to CSS modules here, the first lesson that builds a component spikes whether Astro emits a CSS module imported from inside a remark plugin. If it does not, the fallback is to render the block through an Astro wrapper component. - How do validation errors stay keys? Recommended:
/contractssets a zod error map that turns every issue into a key and parameters ({ code: 'too_small', path, minimum }), the API returns those, and the App translates them with ICU messages. The runtime validations page (D49) records the rule. - Where do locale and time zone come from before a setting exists? Recommended: no profile columns yet, since a nullable column is cheap to add later. I1 sets next-intl's
timeZoneexplicitly ini18n/request.ts(UTC, or the browser's time zone sent in a cookie, decided in I1's brief) so server and browser agree. The columns arrive with the settings screen.
Constraints to keep
- Individual first: one person, every setting optional (D1).
- One interface for every consumer (D48).
- Scope by the cost to add later: decide now only what would force a migration or a rewrite later.
- Pratik types every change; the coach explains. The pass writes docs, not code.
- Datum ids are global and never renumbered. New datums start at D249; check with
node scripts/check-datums.mjs --next. - Retired pages (the one-repo map and its tickets, the Synergy map and tickets) are records and are never edited. New decisions go into live pages only.
- A page whose decision lands in a later lesson describes the target and says what the code does until then, as Centralized Copy does now.
Done when
- Every item above is either updated in its page or answered by a question, and every question's answer is in a live page with a Decision History row and a row in the decisions log.
- The Synergy plan's "Handover from system design" table gains a row for each answer that changes a lesson, and Lesson 0's step 9 lists any extra live paths found.
npm run buildpasses, with the privacy and datum gates.- This page is marked
status: deprecated.