Files
Ben Stull 26f3680197 §22 S6: merge three-tier model into SPEC.md + registry format in DEPLOYMENTS
Write the canonical §22 (deployment → project → RFC collection) into the
binding spec, applying Part A of the design doc and the Part D amendments in
place. Lands the S3 keystone reinterpretation of §B.1/§B.3: a plain granted
account is a granted account, not a global write role; "global RFC Contributor"
is an explicit memberships(scope_type='global') grant; the implicit-public
write baseline is grandfathered onto the migration-seeded default collection
only (N=1 preserved).

- SPEC.md §22.1–§22.14: tiers + isolation, the registry + .collection.yaml
  manifests, content-repo-per-project, per-collection slug identity, collection
  type/initial_state/unreviewed, two-tier visibility (narrow-only), the unified
  {owner, contributor} role vocabulary at {global, project, collection}, the
  four-layer most-permissive union, discovery/joining (invite + request-to-join),
  runtime branding, /p/<project>/c/<collection>/ routing, one inbox,
  per-collection model universe, the default-project+collection migration, and
  §22.14 consolidating the §§1–21 amendments.
- Forward-pointer amendment notes (house style) at §1, §2, §5, §6 routing
  readers to §22.
- docs/DEPLOYMENTS.md: the registry (REGISTRY_REPO / projects.yaml) and
  .collection.yaml manifest formats, the N=1 default-project+collection upgrade,
  and in-app create-project/collection.

Per docs/design/2026-06-05-three-tier-projects-collections.md Parts A/B/D/E (S6).
Docs-only; no code or migration. Releases as part of v0.45.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 01:21:39 -07:00

376 lines
16 KiB
Markdown

# Deployments — building on rfc-app and keeping in sync
This document is the operating manual for downstream projects that
host an instance of the RFC framework. It is the practical
counterpart to [`SPEC.md` §20](../SPEC.md), which carries the
binding policy. If anything here disagrees with §20, §20 wins and
the discrepancy is a bug in this doc.
The audience is the operator of a downstream deployment — someone
who wants to run their own corpus on top of rfc-app and keep that
deployment current as the framework releases new versions.
## What is a "deployment"?
A deployment is a running instance of the framework, hosting a
specific corpus, branded and configured for a specific community.
Each deployment has:
- **A content repo** (separate from this one). Typically holds the
initial RFC markdown files, a `README.md` describing what the
deployment is, a `CONTRIBUTING.md`, and a `.rfc-app-version`
pin. The Open Human Model (`ohm-rfc`) is one such repo.
- **A Gitea instance** the framework talks to. Hosts the meta repo
and the per-RFC repositories the graduation flow creates.
- **A server** running the rfc-app backend + frontend bundle,
pointed at the Gitea instance, with `frontend/.env` and
`backend/.env` configured for this deployment.
- **A pin** — the `.rfc-app-version` file in the deployment's
content repo, declaring which framework version is currently in
flight.
The framework repo (this one) is generic across deployments. The
deployment supplies the values, the content, and the operational
choices. The framework supplies the substrate.
## Starting a new deployment
The shortest path from scratch:
1. **Create the deployment's content repo.** Anywhere git lives —
GitHub, Gitea, GitLab. It will hold the initial RFC markdown
files and the deployment's public-facing README. Add a
`.rfc-app-version` file with the framework version you intend to
pin to (the current `VERSION` from this repo, or any prior
version if you have a reason to pin behind).
2. **Stand up Gitea.** Anything that exposes the Gitea REST API
works; `deploy/RUNBOOK.md` in this repo describes the bring-up.
Create the bot service account and the OAuth2 application.
3. **Check out rfc-app at the version your deployment pins to.**
`git checkout` the tag (or commit) matching the
`.rfc-app-version` you chose. The pin is a contract — your
deployment's behavior is defined by *that* version's code, not
`main`.
4. **Configure the backend.** Copy `backend/.env.example` to
`backend/.env` and fill in the Gitea URL, bot token, OAuth
client credentials, `GITEA_ORG`, `META_REPO`, OWNER login, and
any optional providers (SMTP, LLM keys). The `.env.example` is
the contract for what the framework expects at this version;
read every entry.
5. **Configure the frontend.** Copy `frontend/.env.example` to
`frontend/.env` and fill it in. At minimum, every release
requires `VITE_APP_NAME` (the user-visible name of your
deployment — what it gets called in the browser tab, the header
brand, and the landing H1). The framework ships no defaults for
any deployment-identifying value; if a required variable is
missing, the build fails loudly.
6. **Build and run.** `cd frontend && npm ci && npm run
build`, then start the backend per `deploy/RUNBOOK.md`. The
first sign-in is the OWNER login from `backend/.env`.
7. **Seed the meta repo.** `python scripts/seed_meta_repo.py`
creates the meta repo on Gitea with the initial README,
CONTRIBUTING, and `.gitignore` per §1–§2. This step is one-time
per deployment.
8. **Bring in your content.** Open proposal PRs against the meta
repo to add your initial RFCs, or — if you're migrating an
existing corpus — write directly into the meta repo's `rfcs/`
directory at the version of `.rfc/metadata.yaml` your pinned
framework version expects.
At this point the deployment is running. The content repo holds
the canonical text and the `.rfc-app-version` pin; the server runs
the framework at that pin against your Gitea; the framework knows
nothing about your specific deployment beyond what your `.env`
files told it.
## The registry: projects and collections (§22)
From **v0.45.0** a deployment is **three tiers** — deployment →
**project** → **RFC collection** — instead of one corpus. Most of
the single-corpus setup above still applies (a deployment that wants
one corpus runs the **N=1 case** unchanged), but two things move into
**git-truth the framework mirrors**, exactly the way RFC bodies do.
The binding model is [`SPEC.md` §22](../SPEC.md); this is the operator
view of the file formats.
### The registry repo (`REGISTRY_REPO`) declares projects
`VITE_APP_NAME` and `META_REPO` are superseded. The framework now reads
a **registry repo** — a dedicated repo under your Gitea org, named
whatever you like, whose location you pass in `backend/.env` as
`REGISTRY_REPO` (the framework fails loudly at startup if it is unset).
Its root holds a `projects.yaml`:
```yaml
# projects.yaml (registry repo root)
deployment:
name: Wiggleverse # deployment display name (was VITE_APP_NAME)
tagline: A substrate for collaborative standardization
projects:
- id: ohm # url-stable slug, unique in the deployment → /p/ohm/
name: Open Human Model
content_repo: ohm-content # ONE repo under your org; collections live inside it
visibility: public # gated | public | unlisted (§22.5)
theme: { accent: "#5b5bd6" } # optional per-project token overrides
enabled_models: [claude, gemini] # optional; falls back to ENABLED_MODELS
```
Each project owns **exactly one content repo**. Adding, reconfiguring,
or archiving a project is a PR against `projects.yaml`; the framework's
webhook + reconciler mirror it into the `projects` cache table. Project
**membership** is app state (it churns at user speed), not registry
data. The deployment's display name and tagline come from here, served
at runtime via `GET /api/deployment` — no rebuild needed to rename.
### The content repo declares collections via `.collection.yaml`
A project's collections are **typed subfolders** of its content repo,
each carrying a `.collection.yaml` manifest the registry mirror reads:
```
ohm-content/
model/
.collection.yaml # type: document
rfcs/intro.md
specs/
.collection.yaml # type: specification
rfcs/runtime.md
features/
.collection.yaml # type: bdd
rfcs/login.md
```
```yaml
# ohm-content/model/.collection.yaml
type: document # document | specification | bdd — IMMUTABLE once set
visibility: gated # defaults to the project's; may only NARROW it
initial_state: super-draft # super-draft | active — defaults from type
name: The Model
# enabled_models: [claude] # optional; may only narrow the project's universe
```
`type` is fixed at creation (the framework refuses to change it on a
later mirror). `visibility` may be as strict as or stricter than the
project's, never looser (`public` < `unlisted` < `gated`); reading or
writing a collection requires passing **both** the project and the
collection gate. `enabled_models`, if present, narrows the project's
model universe for that collection.
### Default project + default collection (the N=1 upgrade)
A deployment upgrading from a pre-§22 version is migrated automatically:
its single corpus becomes one **default project** carrying one **default
collection** (`id` `default`, subfolder = repo root), inheriting the old
`type` / `initial_state` / visibility. Old URLs 308-redirect into the
`/p/<project>/c/default/…` form, so existing links survive. Until you
add a second project or collection the deployment is functionally
identical to before, with one extra path segment. The ordered upgrade
actions are the [`CHANGELOG.md`](../CHANGELOG.md) entry's upgrade-steps
block (§20.4).
### Creating projects and collections in-app
Both tiers can also be created from the UI by an Owner — the action
wraps a bot commit (a new content repo + `projects.yaml` entry for a
project; a new subfolder + `.collection.yaml` for a collection), so
everything still flows from git. Nothing becomes app state the mirror
cannot rebuild.
## The two-repo working pattern
Once a deployment is live, day-to-day changes split across the
framework repo and the deployment repo. The triage that decides
which side a given change lands on:
A change is **deployment-side** when it is about a value, a piece
of content, or an operational choice specific to this instance.
Examples: changing the display name; updating an RFC's body;
adding a new RFC; rotating Gitea credentials; tightening SMTP
settings; changing who has the OWNER role.
A change is **framework-side** when it is about behavior, UI
logic, schema, or any piece of code that would matter the same way
to a different deployment. Examples: fixing a bug; adding a new
feature; making a previously hardcoded value configurable; tuning
a retry; correcting a race condition; updating a dependency that
affects observable behavior.
The most common case is **mixed**: the framework needs to make
something configurable, and the deployment needs to supply the
value. The change pattern is then: framework adds the env var (or
config knob) and the loud-failure path, ships a new minor or
patch; the deployment upgrades to the new version and supplies
the value in its `.env`.
When picking up a change, name it in deployment terms first ("in
*my deployment* I want X"), then triage. The phrasing keeps the
work honest — every framework change is in service of something
someone wants from their deployment.
## Upgrading to a new framework version
The upgrade contract is in [`SPEC.md` §20.5](../SPEC.md), and the
normative language used in upgrade-steps blocks is fixed in §20.4.
The mechanics in practice:
1. **Decide the target version.** Read `CHANGELOG.md` from the
current pin up through `main`. Pick the target — usually the
latest, but pinning behind is legitimate if a release introduces
something the deployment isn't ready for.
2. **Read every intervening release's upgrade steps.** Each release
header in `CHANGELOG.md` includes an "upgrade steps" block when
there is operator action required. Read them in order; the
composition is your upgrade plan. **Cross-version upgrades are
not pre-computed** (see §20.4) — going from 0.1.0 to 0.5.0 means
walking the upgrade-steps for 0.2.0, then 0.3.0, then 0.4.0,
then 0.5.0 in that order, applying them as a single plan.
3. **Triage the plan by normative language.** Each step is worded
per RFC 2119 / 8174:
- **MUST / SHALL / REQUIRED** steps are non-negotiable. Skip one
and the deployment is out of contract on the new version.
Either it will fail to build or start (the framework's
§20.6 commitment to loud failures), or it will misbehave
subtly later. Treat these as the binding part of your plan.
- **SHOULD / RECOMMENDED** steps are the framework's tested
default. You can deviate when you have a reason, but
subsequent releases assume you took the recommended path; a
deviation now may complicate a future upgrade.
- **MAY / OPTIONAL** steps are affordances. Take them or skip
them based on your needs; the framework's behavior doesn't
depend on the choice.
- **MUST NOT / SHOULD NOT** steps remove or discourage prior
behavior. Check whether your deployment was relying on the
called-out behavior; if so, that's the thing to change.
4. **Apply the upgrade steps in order against the deployment's
environment.** New env vars get added to `frontend/.env` or
`backend/.env`. Renamed vars get renamed. Removed vars get
removed. Migration scripts (if any are referenced) get run.
5. **Check out the framework at the target version.** `git
fetch && git checkout <tag>`.
6. **Rebuild.** `npm ci && npm run build` for the frontend;
restart the backend.
7. **Smoke-test.** Sign in, check the brand reflects your
`VITE_APP_NAME`, exercise whatever surface the release touched.
Walk through the SHOULD steps that involve verification.
8. **Update the pin.** Change `.rfc-app-version` in the deployment
repo to the target version. Commit. This is the deployment
promising its users that *this* is the framework version
currently in flight.
If a step fails, stop. Do not bump the pin. The framework's
obligation per §20.6 is that a faithful upgrade does not regress
the deployment — failure is a bug to report against the framework
release, not something to paper over.
### Composing across multiple versions
Worked example: a deployment pinned to `0.1.0` wants to move to
some future `0.4.0`. The plan is the concatenation of three
adjacent upgrade-steps blocks from `CHANGELOG.md`:
```
0.1.0 → 0.2.0: MUST set VITE_APP_NAME, MUST rebuild, SHOULD verify
0.2.0 → 0.3.0: (whatever the 0.3.0 entry's steps are)
0.3.0 → 0.4.0: (whatever the 0.4.0 entry's steps are)
```
The operator concatenates these into one plan, applies them in
order, rebuilds once at the end against `0.4.0`'s checkout, and
bumps the pin to `0.4.0`. No path-specific instructions are
needed because each adjacent step is unambiguous in isolation.
This is why §20.4 binds the normative-language convention: the
composition only works mechanically if each adjacent step's
strength is clear from its wording.
## Versioning, briefly
[`SPEC.md` §20.2](../SPEC.md) is the binding rule. Summary:
- **Patch** (`0.x.0 → 0.x.1`): redeploy and you're done. No env
changes, no behavior surprises.
- **Minor** (`0.x.0 → 0.(x+1).0`): read the changelog. New optional
config, new features, and — while pre-1.0 — breaking changes
that the changelog spells out.
- **Major** (`0.x.0 → 1.0.0`, then `1.0.0 → 2.0.0`): always read
the changelog. Breaking by definition; upgrade steps required.
The framework is pre-1.0 today. Expect minor bumps to occasionally
require changes to your `.env` files until the first stable cut.
## The deployment's repo, at a glance
A deployment's content repo typically holds:
- The RFC markdown files (one per concept the deployment defines).
- A `README.md` describing what the deployment is, public-facing.
- A `CONTRIBUTING.md` describing how to participate.
- `.rfc-app-version` — the framework pin (single line, SemVer).
- A `CLAUDE.md` describing how to work on the deployment, the
two-repo iteration pattern, and the deployment-specific values
(typically without secrets — those live on the server only).
The deployment repo does **not** hold:
- Any of the framework's code. The framework lives in its own repo;
you build against it at the pinned version.
- Secrets. `.env` files belong on the server, never in git.
- Forked copies of framework files. If you find yourself wanting to
edit a framework file, the right move is to file a change against
the framework, get a release, and pin to it.
## Private-beta gate
From `0.3.0` onward, every deployment ships with an opt-in email
allowlist. The default state is **off**: an empty `allowed_emails`
table behaves exactly like 0.2.x — any successful Gitea OAuth
provisions a user.
To run a closed beta:
1. Sign in once as the deployment operator so your `users` row exists
(you will be grandfathered by `gitea_id` thereafter — adding the
first allowlist row does **not** lock you out).
2. Open `/admin/allowlist` and add the first invited email. As soon as
any row exists, sign-in is restricted to listed emails plus
grandfathered users.
3. Optionally set `VITE_BETA_CONTACT` in `frontend/.env` (an email
address, a URL, or a short instruction). It is shown to rejected
sign-ins on the `/beta-pending` page so visitors know how to
request an invitation.
To re-open the deployment, remove all rows from `allowed_emails` (the
admin tab has a Remove button per row) — the gate flips off as soon
as the last row is gone.
Anonymous viewers see the full app in read-only mode regardless of
allowlist state. Write affordances (Propose, chat, Contribute, Open
PR) are hidden behind a sign-in CTA, and the public read endpoints
behave the same in both states.
## When something goes wrong
If a framework behavior is wrong for your deployment, file it as a
framework change request: a clear statement of what you wanted, what
happened instead, and (if you can) what the right separation of
concerns is between framework and deployment.
If a framework upgrade breaks your deployment after you followed the
changelog faithfully, that's a §20.6 violation — file it against the
framework release. The pin lets you roll back to the prior version
while the framework fixes the regression.