26f3680197
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>
376 lines
16 KiB
Markdown
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.
|