AmoleoFamily

Process & Registry

Adopted here means the repo has the container-rebuild skill and backlog tooling this tier requires — not the same as “live” (see Family Overview's Status). Website and Rissbrook are both genuinely live with Adopted: No, purely for missing tooling. Login means the site has accounts at all. OIDC isn't shown separately — every login-enabled repo is a client of Amoleo Accounts' own OIDC provider now, not a distinguishing fact per repo (Accounts itself is the one exception, being the provider).

RepoTierAdoptedLogin
Amoleo-WebsitestaticYes—
Amoleo-RissbrookstaticYes—
Amoleo-PetstransactionalYesYes
Amoleo-ConnectionstransactionalYesYes
Amoleo-AccountstransactionalYesYes
Amoleo-Todotransactional—Yes
Amoleo-Editionsundecided—Yes
Amoleo-Upnexttransactional—Yes
Amoleo-Collectionstransactional—Yes
Amoleo-Registrytransactional—Yes
Amoleo-CommunicationstransactionalYesYes
Amoleo-Reviewstransactional—Yes
Amoleo-ImagestransactionalYesYes
Amoleo-Recommendationsundecided—Yes
Amoleo-Worldbooktransactional—Yes
Amoleo-Healthtransactional—Yes
Amoleo-Readertransactional——
Amoleo-Budgettransactional—Yes
Amoleo-Gamesstatic——
Amoleo-Hometransactional—Yes
Amoleo-Milestonestransactional—Yes
Amoleo-Memoriesundecided—Yes
Amoleo-Ameleonsundecided—Yes
Amoleo-Leomalyundecided—Yes

The stack — which one a site gets, and what every repo carries

Five sites on five stacks would be five sets of problems. There are two stacks, and which one a site gets is decided once, by one question, before any code is written.

This document is the authority on why. tools/stack.json is the authority on which repo is which, and tools/check-stack.mjs is what notices when a repo stops being what it says it is.


0. Current registry

The current per-repo values live in tools/stack.json, not in a second hand-maintained table here. The Family guidebook renders that JSON directly, and tools/check-stack.mjs checks the declared shape against checked-out repos. This document explains the rules and reasoning; the JSON records which decision each repo has actually made.

undecided is a legitimate tier for products that have not made a technical-stack decision yet. Do not infer transactional merely because a future product will have accounts or an API; make that decision in the product repo first, then record it here.

Accounts is live. Any login-enabled product is designed to use Amoleo Accounts as its identity provider, but oidc: false in the registry means that integration has not been recorded as implemented in that repo yet. For a live legacy app that can still mean a local-password migration is outstanding; for a design-only repo it simply means there is no implementation yet.


1. Two tiers, and the question that picks one

Does the site keep state that a visitor can change?

If no, it is static. If yes, it is transactional. There is no third answer, and "not yet, but maybe later" is a no — a site that grows a reason to store something gets that decision made properly at the time, in writing, not smuggled in as a serverless function on a Friday.

Static tier

Plain files, served from Cloudflare Pages. No Dockerfile, no server/, no database, no runtime anything — and no edge compute either: no Cloudflare Pages Function, no Worker, no _worker.js, no wrangler.toml/wrangler.jsonc. This was implicit in "no runtime anything" but is worth saying outright after docs-site-src briefly grew a Pages Function (18 Sept 2026) to answer non-GET requests on its /api/* raw-file mirror with a friendlier message than a plain 404 — reverted the same session once it was pointed out that "trivial compute at the edge" is still compute at the edge, and the whole reason this tier is worth defending (see the bullets below) is that nothing runs at all, not that what runs happens to be small. Static tier is not the same claim as "no build step" — Amoleo-Rissbrook has always had one (Astro, static output, no adapter), and Amoleo-Website gained a small one 8 Sept 2026 (npm install && npm run build, copying family-css/*.css out of an installed package — see docs/family-css-system.md's npm-distribution pilot). What actually defines the tier is §1's own question above ("does the site keep state a visitor can change?"), not whether a build step exists at all — but a build step producing files is different in kind from a function that runs on every request, which is why this needed its own sentence rather than folding into that one.

What this buys, and why it is worth defending:

The moment you want a form that posts, a login, a comment, or a counter, you are not on this tier any more. Say so out loud and move the site properly; do not bolt a Worker onto it and call it static.

Transactional tier

One default shape for transactional Family web products:

Part What
Client React, built by Vite, served as static files by the app's own server
Server Node + Express, ESM ("type": "module")
Data MongoDB via Mongoose, in its own container with its own volume
Packaging A multi-stage Dockerfile on node:22-alpine — client build stage, then runtime
Running Docker on the Proxmox LXC, managed through Arcane
Environments Branch-driven: main → prod, develop → dev, test → test

Corrected 4 Sept 2026: this table pinned node:20-alpine until now, and that pin was stale, not current. Node 20 left maintenance in April 2026 and receives no further security patches, including the bundled OpenSSL — confirmed via a pentest-code-reviewer security review of Amoleo-Communications#3, finding C32. node:22-alpine was already Amoleo-Accounts' own deviation from this pin (Decisions.md, 12, originally justified by oidc-provider's Node floor rather than the EOL problem) — that deviation is now promoted to the family default rather than staying a one-repo exception. This is a "new work starts on 22" correction, not a claim that every live container already matches it: Amoleo-Pets and Amoleo-Connections still run node:20-alpine in their own Dockerfiles and each need their own backlog item to bump, same shape of follow-up as the scoped-Mongo-credential item recorded above (§1, "Three things that make sharing safe").

No SSR, no Next, no serverless. The server serves the built client because that is one process and one origin, which removes CORS, cookie-domain and first-paint-ordering problems rather than solving them.

Amoleo-Images is the named exception, on purpose, for a reason specific to it: a CDN edge cannot evaluate a live share-token expiry or a sitter grant, so private and public image bytes have to live in genuinely separate trust boundaries, not just separate routes on one origin. That's Cloudflare R2 (two buckets) plus a Cloudflare Worker for edge-side token verification on the private path — real serverless, real CDN-native storage, unlike every other repo in the family. See Amoleo-Images/docs/Architecture.md and its tools/stack.json entry for the full reasoning. No other repo gets this exception without the same kind of structural reason (a CDN literally cannot do the check another way) — "it would be cheaper" or "it would be faster to build" don't qualify on their own.

Mongo is the only database. Not because it is the best fit for every query anyone will ever write, but because two databases in a five-repo family is four things to back up and two sets of habits. Anything else needs an ADR in the repo that wants it, and a row in the table above.

One Mongo container per environment, shared by every app in it

There is one MongoDB container per environment on the LXC, not one per app. Every transactional app in that environment connects to it over the shared Docker network and takes its own database inside it:

Environment Network Mongo container Pets Connections
Production prod-net mongo-prod petweight kdrama
Development dev-net mongo-dev devpetweight …
Test test-net mongo-test … …

So a connection string is always mongodb://mongo-<env>:27017/<app-database>. Both apps already do this; it had simply never been written down, which is the state a rule is in just before someone breaks it.

Why one container:

Three things that make sharing safe, and are not optional:

  1. The database name is unique across the family. Sharing a container means two apps are one typo away from sharing a database, and Mongo will not complain — it will create the collection and let two schemas interleave in it. check-stack treats a collision between repos as a failure.
  2. Recreating an app never touches Mongo. The Mongo container is separate precisely so that docker rm on an app container is boring. Every container skill has to say so out loud, because the moment someone doubts it they hesitate over a routine deploy.
  3. The container requires authentication, and every app connects as a user scoped to only its own database. This was true in theory and false in practice until Amoleo-Accounts' Phase 1 refinement caught it: mongo-dev and mongo-prod ran with no auth at all, which is a fine risk to carry while every database in the container holds one app's own data, and stops being one the moment a database in that same container holds a password hash. Enabling auth on a shared mongod is atomic across every app connected to it — a repo cannot adopt this on its own schedule, only as part of a coordinated cutover across every app sharing that environment's container. See Amoleo-Accounts/docs/Decisions.md, 11 for the reasoning. Corrected 4 Sept 2026 — this section previously said Amoleo-Pets and Amoleo-KDramaConnections each still needed their own backlog item before the cutover could ship; both since have, and mongo-dev/mongo-prod now require authentication with every connected app — Accounts, Pets, Connections and, as of the same date, Amoleo-Communications — holding a user scoped to only its own database. A new app onboarding onto either container gets its own scoped user created the same way, following Accounts' original precedent; there is no longer a credential-free path onto either container for anyone.

The docker-compose.yml in Amoleo-Connections runs its own mongo:7 beside the server. That is the local stack — a laptop with nothing else running — and it is correct there. It is not how the LXC is deployed, and the compose file should never grow into the deployment path.

If an app ever genuinely needs isolation — a noisy-neighbour problem, an incompatible Mongo version, a legal reason to hold data apart — that is a real answer, and it goes in this section with the reason, not in a container someone quietly stood up.


Amoleo Home / Pulse split

Amoleo Home follows the transactional family tier and requires Accounts sign-in because it stores private durable household knowledge for a person. Amoleo Pulse is different: it is a self-hosted local runtime/satellite and is intentionally outside this family stack registry.

Home may pair to Pulse, but Pulse authenticates with a Home-issued installation credential rather than an Accounts machine-to-machine grant. Pulse initiates the connection outbound and must continue local automation if Home or the internet is unavailable. Do not infer from Home's family-stack rules that Pulse needs Mongo, Accounts, the family footer, or public hosting.


2. Login — Accounts is the identity provider, the app still owns its user row

Any site with a login uses Amoleo Accounts as its OpenID Connect provider. No app implements its own password flow, its own reset email, or its own password hashing. Legacy live apps may still be completing that migration; the registry records actual adoption state. That does not change the pattern. See Amoleo-Accounts/docs/Architecture.md and docs/Migration.md.

Each app keeps its own user table anyway. That is not a contradiction, it is the split:

Lives in Accounts Lives in the app's user table
Who this person is: the sub, email, verification, password, sessions Everything this app needs about them: preferences, tier, sort orders, their pets, their board

Three rules that make the split hold:

  1. The app's row is keyed by the OIDC sub, not by email. Email changes; sub is forever. A table keyed on email quietly becomes two accounts the first time someone changes theirs.
  2. The app never stores a credential. No password hash, no reset token, no "just in case" copy. If the app can authenticate someone without Accounts, Accounts is not the identity provider — it is a suggestion.
  3. Profile fields are cached, never authoritative. An app may hold a copy of the display name to render a page without a round trip; when it disagrees with Accounts, Accounts wins.

A static-tier site cannot have a login. If it wants one, see §1 — it has changed tier.


3. What every repo carries, regardless of tier

A GitHub repository

Under ValadAmoleo, normally named Amoleo-<Product>. Legacy folder names and temporary rename exceptions are recorded in tools/repos.json; do not infer the on-disk folder from the product name. Local -only work is not a state any of these repos are allowed to be in: the backlog, the issues and the review history all live at the remote, and a repo without one has none of them.

The backlog skill

Every repo runs the /github-project-backlog workflow: a GitHub Projects v2 board with the Status/Priority/Effort fields, and scripts/backlog-tools/ in the repo so work can be picked up, reordered and closed without a human relaying it. Registered repos are tracked in tools/repos.json; backlog/tooling adoption is checked mechanically where the repo is available locally rather than maintained as a second prose list here.

This is the one item on the list that looks like overhead for a small repo and is not. A static site's work is small, infrequent and therefore forgettable — the SEO fix noticed in June and remembered in October is the normal outcome without a board.

A container skill, if it has a container

Every Docker repo carries a recreate-prod-container command in .claude/commands/ that produces the full docker run invocation — every volume, every env var, every secret prompted for rather than assumed. Both apps have one, and both should keep them in step with the Dockerfile in the same commit that changes it.

The reason is narrow and real: recreating a container is a thing done rarely, under pressure, and an env var forgotten during it is a feature that silently stops working. The skill also has to state plainly what is not destroyed — Mongo lives in a separate container and is never touched by recreating the app.

A CLAUDE.md pointing here

Every repo's CLAUDE.md names Amoleo-Family and says what lives there. See §5.


4. The rest of it, written down once

Smaller than the tier decision, big enough to diverge on:


5. Pointing back here from a repo

Each repo's CLAUDE.md carries a short Part of the Amoleo family section near the top: what the family repo is, where it is on disk relative to that repo (../Amoleo-Family), its GitHub URL, and the specific list of things that live there rather than locally — brand colours, the footer lockup, cookie consent, screenshots, and this document.

Two things make the pointer worth the lines it takes:

check-stack.mjs checks each repo has one, by looking for Amoleo-Family in its CLAUDE.md. That is a weak check — it catches a repo with no pointer, not a pointer gone stale — and it is honest about being one.


6. What the checker cannot see

check-stack reads files on disk. It can tell that a repo declared static has grown a Dockerfile, that an app lost its container skill, that a repo has no GitHub remote or no backlog tooling. It cannot tell you whether the OIDC integration is correct, whether the user table is really keyed on sub, or whether a secret is hard-coded three directories down.

So the rules in §2 and §4 are enforced by reading, which means they are enforced by this document being short enough to read. Keep it that way: when something here can be made mechanical, move it into the checker and cut the prose to a sentence.

Generated from docs/stack.md + tools/stack.json. Edit there and re-run npm run build in docs-site-src/ — never hand-edit a page.