A tier removal with holders ended, aligned and materialized every holder's pool inside one transaction that held the exclusive rendezvous, so its hold grew with the holders and had no limit (#187). Under Keep it removed the last tier of a ladder an org type defaults to, leaving that type's default plan empty; under Migrate the same removal failed at a pool step. Its four step failures answered "Details are in the server logs." RemoveTier now runs on runPlanAct. Inside the exclusive transaction it: - counts the live holders, reads the ladder's tiers and the org types defaulting to it, and refuses through TierRemovalRefusal before any pool row. That function refuses above planActHolderCap, then refuses the last tier of a default ladder under either disposition. A failed read refuses too. - reads the product's and the ladder's names, refusing when they cannot be read; - locks the holders' pools in ascending id and reads them again, then applies the disposition gate; - records the act (tier_removed, with the disposition when default-sourced holders remain), deletes and renumbers, and reconciles each pool. The floor grants Migrate creates carry the act's id. The limit refusal offers "changing that default, or reordering this ladder, with Migrate" only when that advice is true: when the tier is rank 0 of a ladder with another tier and every default-sourced holder's type defaults to it. Otherwise it offers the default change alone. The removal preview reads the same counts and calls the same function with the same limit. A refusal renders "Apply change" disabled with the sentence as its reason. A failed read of the org types or the tiers answers "Failed to preview the removal. Reference: {request id}." in place of the preview, and a failed holder list answers "Failed to load the tier's holders. Reference: {request id}.". The promoted-default note shares the preview's one org-types read. CommitPlanLadderTierRemoval only parses, calls and renders. It answers as follows: - Every failing step is logged through the request's logger with the step, the ladder, the product and the pool or provision. - The four step sentences end "Reference: {request id}.". - A lock wait or deadlock answers "Another change collided with this one; try again.". - A removal with no holder left to reconcile answers "{Product} removed from the ladder.". - A 422 whose preview cannot be rebuilt answers the preview's own read failure. - A gate that was true while the rebuilt preview finds no default-sourced holder answers the generic failure, and logs an Error line with the request id. The design's removal table gains those two rows, and copies 14, 16 and 52 name their new places and condition. docs/database-locks.md lists the six paths that render the collision sentence. docs/plan-management.md gives the removal's limit, its last-tier refusal and its repeated-removal no-op. Goldens: - Behaviour changes in remove-begin-fails, remove-second-pool-fails, remove-keep-rank-0, remove-migrate-pool-held-elsewhere, remove-other-source, remove-migrate-rank-0, remove-two-pools, remove-no-holders-left, remove-last-tier-keep (refused, where it used to empty the default ladder), remove-last-tier-migrate (refused before any pool step), remove-product-name-unread, contention, the four removal-against-default files and tier-change-during-conferral/a-tier-removal. - New: remove-above-the-limit and remove-org-types-unread. - remove-needs-disposition and remove-refused-disposition do not move: the gate writes nothing. Task 8.5's text is corrected to say so. The full suite passes.
member-console
One system for memberships and the hosted services they pay for.
member-console is open-source software for offering hosted services on a paid membership. In one place it handles the pieces that otherwise get stitched together by hand — who each member is, what they're entitled to, what they pay — then turns that into the actual, metered services running on their behalf, kept in sync as members join, upgrade, and leave. New kinds of service attach through a shared contract instead of reshaping how membership or billing work, so the offering can grow on one foundation.
It has two sides: an operator panel where staff run the service, and a member panel where members sign in to manage their own plan and workspace.
Developed by the Wiki Cafe Multistakeholder Cooperative, and built for any organization to run.
The operator landing surface
Status: pre-production. The engine is feature-complete for its first service tiers, but this is not yet a hardened, turnkey deployment. Expect rough edges in first-run setup. docs/production-deployment.md sequences a real path to a running production instance, but it is a self-hoster's guide to follow and adapt, not a one-command install — the test stack remains the fastest way to see the console running. See
status/for the honest state of every milestone.
License: AGPL-3.0-only, with commercial licensing available — see LICENSE and COMMERCIAL.md.
What it does
- Governance spine — identity (via any OIDC provider) → organization → workspace, with auto-provisioning of a default workspace for new members.
- Entitlements — grants resolve to numeric limits (e.g. FedWiki site count and storage) materialized per resource pool; plan ladders model upgrade/downgrade paths with a generic transition primitive.
- Billing — Stripe is the billing execution engine. Products, prices, customers, subscriptions, and invoices are mirrored via a webhook pipeline; the console never holds card data.
- Integrations — an integration contract (manifest, namespaced resource keys, lifecycle verbs) lets a new hosted service conform without reshaping the operator panel or entitlement layer. FedWiki (wiki site provisioning) and Discourse (forum group membership) are the two conforming integrations today.
Architecture
- Go 1.23, server-rendered HTMX multi-page app — no SPA framework — under a
strict Content-Security-Policy (
script-src 'self',style-src 'self'; all JS and CSS is self-hosted, no inline<script>/style=). - PostgreSQL with a schema-ownership boundary between core and integrations:
the console's own data lives in a single
coreschema, and each integration owns its schema (fedwiki,discourse,stripe) and its own migration ledger — so adding one renumbers nothing (core-first is the only ordering rule). Queries are generated with sqlc; migrations run with goose. - Temporal for durable workflows (provisioning, integration sync, billing sweeps).
- OIDC for authentication — member-console is identity-provider-agnostic and works with any OIDC-compliant IDP (the test stack ships Keycloak).
Full documentation is indexed in docs/, grouped by audience
(developer / admin / user). Highlights:
deployment architecture,
hosting,
building an integration,
database management, and Stripe.
Running it
What you need:
- At least one conforming integration — FedWiki (wiki site provisioning) and Discourse (forum group membership) are the two available today (see "What it does" above); a deployment with neither has no product to actually deliver to a paying member. See fedwiki-setup.md and discourse-setup.md.
- An OIDC identity provider — any OIDC-compliant provider; the console authenticates members and operators against it and never stores passwords itself. See identity-provider-setup.md.
- PostgreSQL, Valkey, and Temporal — the console's own data store, session store, and durable-workflow engine (provisioning, integration sync, billing sweeps). See database-management.md and deployment-architecture.md.
- Stripe — optional, only needed to sell paid products; a deployment offering only free or unmetered products can skip it. See stripe.md.
First run, in order:
- Deploy the console and the dependencies above — see docs/production-deployment.md for the sequenced path from a built image to a running instance.
- Sign in as an operator (the account holding the
operator-memberrole configured on the identity provider). - Follow the in-app setup checklist at
/operator/setup, which walks through everything still needed to reach a deliverable product.
Quickstart (local development)
Prerequisites: Docker (with Compose), Go 1.23+, and make.
The test stack brings up Postgres, Valkey, Temporal, and Keycloak in
containers, then runs the console from source against them. All commands run
from the test/ directory:
cd test
./bootstrap-stack.sh # generate a per-worktree .env with collision-free ports
docker compose up -d # Postgres, Valkey, Temporal, Keycloak
set -a; . ./.env; set +a # load the generated host ports / secrets
go run .. start --config mc-config.yaml
Integrations are opt-in, not baked into the stack: a FedWiki farm or a live
Discourse forum is composed by adding its name to the COMPOSE_PROFILES knob
in the generated test/.env (and uncommenting that integration's MC_* block
in the same file). The console runs fine with neither.
bootstrap-stack.sh prints the URL table it allocated. With default ports the
console is at http://member-console.localhost:9481; sign in to the operator
panel as alice / password.
On a fresh stack the console rides out Temporal's cold-start with a bounded connect retry, so the first
startmay pause briefly before the server comes up — no re-run needed.
See test/AGENTS.md for the full stack contract (idempotency,
Stripe webhook constraints, teardown, running multiple worktrees concurrently).
Project status & roadmap
See status/ for the development roadmap, milestones, and notes.
Substantial changes are tracked as OpenSpec proposals
under openspec/.
Contributing
Contributions are welcome — see CONTRIBUTING.md for the development workflow, building and testing, and the project conventions.
Security
To report a vulnerability, see SECURITY.md. Please do not open a public issue for security reports.
License
member-console is free software, licensed under the GNU Affero General Public
License v3.0 (AGPL-3.0-only). See LICENSE for the full text.
AGPLv3 is and will remain the default license for this project. If you use, modify, or deploy member-console — including over a network — you must comply with the AGPLv3, which includes making the corresponding source available to users of the service.
If the AGPLv3 does not fit your needs — for example, you want to embed member-console in a proprietary product or run it as a hosted service without the AGPL's source-sharing obligations — a separate commercial license is available. See COMMERCIAL.md.
As the sole copyright holder, Christian Galo can offer member-console under both the AGPLv3 and separate commercial terms. Contributions are accepted under a Contributor License Agreement that preserves that ability (CLA.md; how to sign is in CONTRIBUTING.md), and commercial licenses fund continued development. Anything already published under the AGPLv3 stays under the AGPLv3; the public license is not withdrawn retroactively.
Third-party components redistributed in this repository keep their own licenses. See NOTICE.
