kamp.us, reborn.
A multi-app, multi-worker repo: one Cloudflare Worker per app under apps/
(web is the only app today), each its own package + stack + stage (ADR
0057). React 19 + Effect + fate. HTTP via Effect HttpRouter / HttpApiBuilder
(ADR 0027). Durable Objects authored on alchemy's Effect DO model (ADR 0028).
- One worker per app. Each
apps/<app>is its own pnpm package owning its ownalchemy.run.tsstack + per-app stage, reusing the account-global state store and the four CI secrets — no second bootstrap (ADR 0057).apps/webis the only worker today; the structure fans out as apps are added. apps/webserves both the SPA (viaassetsbinding) and the API.- The data layer is fate's native protocol:
/fateserves data views,/fate/livedrives live views over SSE. Other backend routes live under/api/*. - Frontend is React 19 + Vite, built into
dist/client. - DOs are bindings on the same worker: a single unified
LiveDO(ADR 0037, on the Effect DO model of ADR 0028) plays both the connection and topic roles to power the fate-live SSE fan-out (ADRs 0023/0025). Add more DOs per feature.
phoenix/
├── apps/ # one worker per app, each its own package + stack (ADR 0057)
│ └── web/ # @kampus/web — the only worker today
│ ├── worker/ # worker entry + backend code
│ ├── src/ # React frontend
│ └── alchemy.run.ts # this app's alchemy stack (replaces wrangler.jsonc)
├── packages/ # shared internal packages
├── infra/ # standalone stacks: ci-credentials (one-shot CI-token provisioner), depo (internal asset store/CDN — designed, ADR 0144)
└── pnpm-workspace.yaml
pnpm install
cp apps/web/.env.example apps/web/.env # first run only — local dev env (gitignored)
pnpm dev # turbo-driven; two processes: `vite` (SPA/HMR) + `alchemy dev` (worker)
pnpm dev:web # just the Vite SPA dev server
pnpm dev:worker # just `alchemy dev` (the worker on a local workerd, offline)
pnpm build
pnpm deploy # pnpm build && alchemy deploy (use --stage <name> for isolation)
pnpm typecheck
pnpm lint # biome check
pnpm format # biome check --writealchemy dev auto-loads apps/web/.env (it layers a .env over process.env), so BETTER_AUTH_SECRET (a required Config.redacted, no default) and ENVIRONMENT come from there — copy .env.example → .env once. Production secrets are Cloudflare secret_text bindings set by alchemy deploy, never read from .env.
Deploy is alchemy-managed (ADR 0026–0031): alchemy.run.ts is the stack, there is
no wrangler.jsonc. alchemy deploy --stage <name> yields an isolated worker + D1
- DOs per stage; CI uses the Cloudflare-hosted state store, local dev uses
Alchemy.localState()(offline).
- All commands use
pnpm. - Never use
npx ...; usepnpm dlx ....
- Tech is rebuilt from the
kamp-us/kampusrepo (worker + DO patterns). - Products are reborn from the
kamp-us/monoreporepo (sozluk, pano, kampus). - The shape is
kampus, the products aremonorepo, collapsed into theapps/webworker.
- Biome formatting: tabs, 100 col, no bracket spacing.
- Node over Python for scripts/hooks. Mechanical tooling lives as an Effect CLI package under
packages/(theepic-ledger/crabbox-manifest/leak-guardidiom —effect/unstable/cli, run withnode src/bin.ts), not an ad-hoc script or a Python hook. A pure, unit-tested core + a thin Effect bin; never a one-off.py. The tools fold into one router,fabrika <group> <verb> …(packages/fabrika-cli/); before hand-rollinggh/jq/gitglue, reach for an existing verb —fabrika --helplists the groups,fabrika <group> --helpthe verbs under one (derived from the registry, never auto-injected). Shell that must exist anyway (a workflowrun:block, a git hook body) follows the bash-3.2 shape in.patterns/skill-script-shell-shape.md—set -uo pipefailnever-e(errexit plus a cleanupEXITtrap launders aset -uabort into exit 0, so a guard passes on the path it aborted in) — and per ADR 0228 such shell may relay a verb's answer, never derive the decision itself. (The sourced-orchestration-script exception that ruling carved out for the v1kampus-pipelineskills retired with that plugin — fabrika skills callfabrikaverbs directly.) - Effect for backend control flow; feature services are isolate-level layers, with the per-request services (
CurrentUser,LivePublisher,CurrentActor) provided onto each handler from the validated session (ADRs 0029/0041;CurrentActorper ADR 0107 §7).Authis a BetterAuth type alias, not the per-request session carrier —CurrentUser(ADR 0042) is. - Make invalid states unrepresentable. Domain logic in domain objects.
- Comments earn their place or die. Code must not be buried between comments a reader pattern-matches as boilerplate and skips — a skipped comment is pure noise that rots unread. Not anti-comment: a load-bearing note is the point. But the why belongs in
.decisions/, how-the-code-is-shaped in.patterns/; an inline comment is the surface of last resort, for a note with no other home that belongs at this exact line (a local invariant at its enforcement site, a workaround + its forcing constraint, a deliberate-looking-wrong guard, a pragma rationale). Cut separators, name-restaters, and narration of obvious control flow; collapse a docblock that re-derives an ADR's why to a pointer (// See ADR NNNN). A top-of-file docblock is fine when it states what the module is + the one non-obvious thing — not an essay re-deriving the code. Enforce with thedeslop-commentsskill (/skill:deslop-commentsin pi; opencode loads the same skills viaopencode.jsonand the agent shells under.opencode/agent/). - Ground falsifiable claims about platform/runtime/dependency behavior in source, not intuition. Any decision-driving claim about how a platform, runtime, or dependency behaves — D1/workerd/CF-isolate semantics, an engine's tokenizer/collation, a binding's resolution, a library's API contract — that an ADR or a diagnosis rests on must be verified against the authoritative source (the dep's source/docs, a spec, or an actual test against the real platform) and cited, never asserted from intuition. (ADR 0040 rested on the unverified "
node:sqliteis the same engine as D1"; ADR 0082 then had to tear out the four-tier taxonomy it grew into.) The canonical instance: ground Effect API/design decisions inEffect-TS/effectmain'sLLMS.md(and itsai-docs/examples) over intuition — when the documented idiom and a "cleaner" instinct conflict, the documented idiom wins; cite it by section. Effect 4 was prototyped in a separate repo and folded back intoEffect-TS/effectmain, which is now the one live address; phoenix's4.0.0-beta.92pin predates the beta-to-rc cut, so where main's API has moved on, read the module source at the pin. Deviations from a grounded source must be justified by a real platform constraint (e.g. CF isolates have no shutdown hook), not preference. - If you rely on a pattern not yet in
.patterns/, add or extend a doc for it (per the "When to add a new pattern doc" criteria in .patterns/index.md) — don't leave a load-bearing pattern undocumented. - In-repo docs: standard markdown links (
[text](.patterns/index.md)), not Obsidian[[wikilinks]]; use real resolvable paths, no placeholders. - Doc surfaces:
README= project/product front door (what kamp.us is — products + ethos; never carry retired or old-problem context a new reader has no frame for);DEVELOPMENT.md= current build/dev state for builders (quickstart, stack, architecture, commands, conventions, the pipeline);.decisions/= the why + history, including superseded approaches;.patterns/= how the current code is shaped;reports/= dated point-in-time findings/measurements (analysis snapshots — not evergreen, distinct from.patterns/code-shape and.decisions/why+history);.glossary/= the canonical vocabulary (architecture terms + product/brand nouns);design-system-manifest.md= the context-file-for-design — the four-pillars design law (ADR 0162) as an agent-readable manifestwrite-codereads before generating any UI (role-token annotations, component-selection rules, per-pillar prohibitions). - Every
packages/*workspace package carries aREADME.md(what it is, why it exists, how to use it) — a package with no README has no entry point for a reader or consumer. Enforced fail-closed in CI byfabrika guard readme-guard check(thereadme-guard.ymljob), which scopes to real workspace members (dirs with apackage.json) so it ignores dead-shell dirs and fails closed on zero scope (ADR 0092). - A mutation over a fate-live fanned entity must publish the
/fate/liveinvalidation. AFate.mutationthat writes an entity in a subscribed connection (Post/Comment/Definition) must, after the write, publish throughWorkerLivePublisher— omitting it silently staleness-breaks every other client's live view (the publisher's error channel isnever, so nothing forces it; #1893–#1896 all shipped the omission). Every mutation is classified fanned/not inapps/web/worker/features/fate-live/fanned-mutations.ts, andfabrika guard fanout-guard check(thefanout-guard.ymljob) fails closed on an unclassified mutation, a fanned mutation whose feature omits the publish, or zero scope (ADR 0155/0092). - Decisions are product-driven by default; engineering leads only on platform/infra (the pipeline, fate/DO substrate, infra primitives) — see ADR 0078.
- Turkish for product/brand, English for technical. Product/brand names and user-facing copy stay Turkish; everything technical is English — URL routes/paths, code identifiers, D1 table/column names, file names. The canonical vocabulary lives in
.glossary/LANGUAGE.md— read it; the brand-noun list and the architecture terms (module / interface / depth / seam / …) are defined there, not duplicated here. - Every dependency via
catalog:. Each dep in anypackage.jsonis sourced from the pnpm workspacecatalog:(declared once inpnpm-workspace.yaml), never a hardcoded version string — one shared version per dep across the repo. When a dep is also a transitive dep of something already in the tree, catalog it at the EXACT version that parent links (don't introduce a second version). Enforced fail-closed in CI byfabrika guard catalog-guard check(thecatalog-guard.ymljob), which scans the root and every workspace memberpackage.json(dependencies/devDependencies/peerDependencies) and reds on any non-catalog:/workspace:version or on zero scope; a genuinely unavoidable exception lives in the guard's explicit reasoned allowlist, never a silent tolerance (ADR 0092; #2737). Incident: PR #535 hardcoded@distilled.cloud/cloudflareand broke frozen-lockfile CI.
The ADRs live in .decisions/ — one NNNN-slug.md per decision, the why in each file's body. There is no committed index (ADR 0126) and no SessionStart ADR-map hook (ADR 0129, dropping 0126's hook as needless indirection): discovery is this contract, the same in every context (session, subagent, CI). Discover ADRs by ls .decisions/ — the NNNN-slug filenames are the map — plus each file's frontmatter (id/title/status) for the row. Open the file when you need the why. Record new decisions with /adr.
There is no on-demand id · title · status map today. v1's decisions-index compact printed one and was deleted with its package (#6100); fabrika ships no replacement (adr next / adr resolve / adr sweep / guard decisions-index validate answer other questions). So ls plus frontmatter is the whole discovery contract until one is built — ADR 0305 records the retirement and #6332 tracks the replacement. Stated rather than left silent, because ADR 0129 makes this section the contract, and a contract naming a command nobody ships sends its reader nowhere.
See .glossary/LANGUAGE.md — the canonical architecture vocabulary (module / interface / implementation / depth / seam / adapter / leverage / locality + the deletion test), extended with phoenix's own structural terms (the two test tiers — unit / integration, the fate loader/resolver split, the LiveDO connection/topic roles) and the product/brand nouns. This is the single source for those terms; don't redefine them inline.
See .patterns/index.md — evergreen patterns for writing phoenix backend code (services, errors, testing, layer wiring). Read before adding a new feature or service.
When the docs and apps/web/worker/ disagree, the source is authoritative — fix the doc.
The moment you spot work you won't do right now — a bug, a refactor you're not here to make, a design question, an investigation, a missing test, a confusing convention — file it as a GitHub issue with the report skill, then return to your task. Do this autonomously and in-the-moment: don't ask permission, don't propose-first, don't wait until you're "done" (by then the observation is gone). The skill files a type-blind issue tagged status:needs-triage and nothing else — classifying and prioritizing is triage's job, not yours. This is the only sanctioned way observations leave a session; a follow-up that lives only in the conversation is a follow-up that dies there.
There is no seeding mechanism in the worker, and cold-start content seeding is a founder-declared v1 non-goal: the first cohort is the two founders writing as users, and new yazars arrive by vouch (kefil) + moderation — no imported/seeded corpus is planned. Security guard (load-bearing): no runtime seeder route may be rebuilt on the public worker — the deleted ENVIRONMENT-gated /api/admin/* seeder routes + the import-sozluk/import-pano scripts were a fail-open security hole.