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-TodotransactionalYes
Amoleo-EditionsundecidedYes
Amoleo-UpnexttransactionalYes
Amoleo-CollectionstransactionalYes
Amoleo-RegistrytransactionalYes
Amoleo-CommunicationstransactionalYes
Amoleo-ReviewstransactionalYes
Amoleo-ImagestransactionalYesYes
Amoleo-RecommendationsundecidedYes
Amoleo-WorldbooktransactionalYes
Amoleo-HealthtransactionalYes

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. Status — where each repo actually is

Repo Tier Stack Hosted Login Backlog Container skill
Amoleo-Website Static Hand-written HTML/CSS/JS + a small npm install build for @valadamoleo/family-css (8 Sept 2026) Cloudflare Pages No Yes n/a
Amoleo-Rissbrook Static Astro 7, static output, no adapter Cloudflare Pages No Yes n/a
Amoleo-Pets Transactional React + Vite / Express / MongoDB Docker on the Proxmox LXC Own table, local passwords Yes Yes
Amoleo-Connections Transactional React + Vite / Express / MongoDB Docker on the Proxmox LXC Own table, local passwords Yes Yes
Amoleo-Accounts Transactional React + Vite / Express / MongoDB Docker on the Proxmox LXC It is the login Yes Yes

One gap left, recorded rather than hidden: Pets and Connections still run their own password tables and haven't moved to Accounts OIDC, even though Accounts itself is now built and live. check-stack prints that as pending, and --strict prints it as the failure it will become. The backlog column used to read Missing for all three of Website, Rissbrook and Accounts — all three have since run /github-project-backlog (Website Project #13, Rissbrook Project #14, Accounts Project #7), but the table and tools/stack.json went stale after that setup landed and weren't corrected until 3 Sept 2026, which is exactly the kind of drift this document exists to catch.

Accounts' tier is decided: Amoleo-Accounts/docs/Decisions.md, 10 puts it on the transactional tier, same shape as Pets and Connections, on the family's own Docker host — chosen over handing identity to a managed provider (Auth0, Clerk, a hosted Authentik/Keycloak/Zitadel). The container has since been built: adopted in stack.json flipped to true via #9, and CI (#18) publishes the image it pulls. This row said "design only, no code yet" for some time after that stopped being true — corrected 3 Sept 2026, prompted by a user catching the same drift in this repo's backlog-tooling claim.


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. 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.

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 shape, and both apps already have it:

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.


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. Today both apps do exactly that; that is the migration, not 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, named Amoleo-<Product> — which for three of the five is not the folder name on disk. The mapping is in the family CLAUDE.md. 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. All five repos on this page have it — Pets and Connections first, then Website (Project #13), Rissbrook (Project #14) and Accounts (Project #7).

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.