Kins & Co Matrix Stack // developer architecture

What you need in your head before touching the backend: repo boundaries, service topology, the deploy pipeline, the invariants CI enforces, and where the sharp edges are.

repo: josikinzz/kins-co-matrix (private) · platform: Railway "Kins&Co" · updated 2026-07-04 · see CONTEXT.md for canonical vocabulary

Two repos, one hard boundary

Everything server-side lives in kins-co-matrix. The public web edge in front of it lives in josiekins. Almost every confusing question about this system resolves to knowing which side of that line a thing is on.

kins-co-matrix — the building

Service definitions (Dockerfiles, configs, ops scripts) for all 7 Railway app services, DoseBot's full source, ops contracts + validation, health/recovery workflows. Railway deploys straight from this repo. Granting a sysadmin access = adding a collaborator here.

josiekins — the front door

The Matrix Routing Layer only: Vercel rewrites in vercel.json, .well-known/matrix/* discovery generation, the /matrix-info onboarding page + server-map generator, and routing-parity tests. No server code.

The one cross-repo seam: Cinny's nginx default.conf (owned in kins-co-matrix) must agree with josiekins' discovery contract (matrix/contracts/matrix-discovery.json) on the homeserver, auth, and RTC endpoints. There is no automated check across the repos — it's a documented manual invariant. If you change either side, check the other.
Never commit secrets, tokens, or private admin URLs to either repo — private ≠ safe. Runtime secrets live only in Railway service variables and GitHub Actions secrets. Local credential notes live in gitignored matrix/*.local.md files.

System topology

Three layers: a Vercel edge that owns the public hostnames, seven Railway app services, and two managed Postgres instances. Federation traffic from other homeservers enters at the same public endpoints.

Edge · Vercel (josiekins repo)
josiekins.xyz
Server Name (user IDs) · serves .well-known discovery + /matrix-info
matrix.josiekins.xyz
Homeserver Endpoint · rewrites to Synapse + MAS-owned auth paths
chat.josiekins.xyz
Cinny web client
▼  rewrites / proxy  ▼
App services · Railway project "Kins&Co"
synapse
Matrix homeserver · rooms, events, federation, admin API
mas
Matrix Auth Service · OIDC-style auth, registration, sessions; Synapse delegates auth here
cinny
nginx serving the Cinny SPA · its default.conf must match the discovery contract
matrix-rtc-focus
Issues short-lived LiveKit JWTs after verifying room membership
livekit
SFU carrying MatrixRTC call media
dosebot-matrix
Bot runtime · onboarding, admin reconciler, join-rule gate (healthz on /healthz)
emoji-pack-sync-job
Job service · merges/syncs custom emote packs via maintenance-bot token
▼  persistence  ▼
Data · Railway managed images
postgres (synapse)
All room state + message history
postgres (mas)
Accounts, sessions, auth grants
Reading the routing: the authoritative host/path → upstream map is josiekins' matrix/contracts/matrix-upstreams-and-routes.json, rendered into both root vercel.json and the generated dist/vercel.json (kept equivalent by the Routing Parity check). Don't reverse-engineer routes from behavior — read the contract.

Where things live in kins-co-matrix

kins-co-matrix/ ├── AGENTS.md / CLAUDE.md / CONTEXT.md ← agent guidance + canonical glossary ├── ARCHITECTURE.md / README.md ├── .github/workflows/ │ ├── ci.yml ← dosebot tests + dist-sync + matrix:validate │ ├── matrix-health.yml ← 10-min probe, opens GitHub issue on failure │ └── emoji-pack-sync.yml ├── matrix/ │ ├── SKILL.md ← the ops runbook; read before server work │ ├── synapse-service/ ← canonical maintenance scripts live here │ ├── mas-service/ · cinny-service/ · livekit-service/ │ ├── matrix-rtc-focus-service/ · emoji-pack-sync-job-service/ │ ├── contracts/ ← ops contracts (JSON), validated by matrix:validate │ ├── validation/ ← validate-contracts.mjs, live-health/auth/smoke, sync-duplicate-scripts.mjs │ ├── archive/ ← frozen history: retired force-join cron, MAS cutover, old handoffs │ └── *.local.md ← gitignored credential notes (never committed) ├── railway/ ← rebuild guide + env var examples per service ├── docs/agents/ ← issue tracker / triage labels / domain-doc conventions └── services/dosebot/ ← full bot source; own AGENTS.md, tests, committed dist/

DoseBot's dist/ is committed on purpose: Railway runs exactly what CI verified, and verify:dist-sync fails the build if dist/ drifts from a clean tsc build. Rebuild and commit dist/ with every source change.

Push-to-deploy: git push is the deploy button

Every app service is GitHub-connected to main with a scoped root directory (its Docker build context — Railway builds with the service dir as root, so a Dockerfile cannot COPY from sibling dirs) and watch patterns (only a matching diff triggers that service's rebuild). Push once; only affected services rebuild, health-check, and swap in.

Railway serviceRoot directoryRebuilds when you touch
synapsematrix/synapse-servicethat dir
masmatrix/mas-servicethat dir
cinnymatrix/cinny-servicethat dir incl. nginx default.conf — check discovery contract on change
livekitmatrix/livekit-servicethat dir
matrix-rtc-focusmatrix/matrix-rtc-focus-servicethat dir
emoji-pack-sync-jobmatrix/emoji-pack-sync-job-servicethat dir job service — builds image, runs on schedule/demand
dosebot-matrixservices/dosebotthat dir healthcheck /healthz, 300s timeout
postgres ×2managed registry images — not built from the repo
railway up is an emergency fallback only. Manual uploads bypass CI and desync the repo from production. If you must use it (documented in matrix/SKILL.md), reconcile the repo immediately after.

Contracts & guards — what CI will hold you to

Operational invariants are written down as JSON contracts under matrix/contracts/ and enforced by npm run matrix:validate (locally and in CI). The ones you'll actually hit:

  • Railway service manifest — the declared service set, roots, and roles must match the service dirs that exist.
  • Duplicate script guard — four maintenance shell scripts must be byte-identical between synapse-service/ (canonical) and emoji-pack-sync-job-service/ (copy, required by the build-root constraint). Edit the canonical copy only, then npm run matrix:scripts:sync. Never hand-edit the copies.
  • DoseBot adapter contract — required env names must stay documented in services/dosebot/docs/matrix-runtime.md.
  • Bootstrap invariants — literal content checks on bootstrap-synapse.sh/bootstrap-mas.sh (feature flags, env wiring), folded into a declarative contract in place of the old inline grep wall in run.sh.
  • dist-syncservices/dosebot/dist/ must equal a clean build (npm run verify:dist-sync).
  • DoseBot test suite — 251 tests via tsx --test, run serially (--test-concurrency=1). CI runs them on every push.

Every contract is one entry in the Contract Registry (matrix/validation/scripts/lib/registry.mjs): 8 contracts total, each an ordered { name, path, assert } entry. matrix/validation/scripts/validate-contracts.mjs itself is a ~30-line dispatcher that just loads the registry and runs it — adding a new Ops Contract is a one-entry registration, not new dispatcher code.

Fast pre-push check: npm run check at the repo root = matrix:validate + DoseBot tests + build. If that's green, CI will almost certainly be green.

Invite-gated access, mechanically

The whole community hangs off one root space. Its join rule is invite; every community room is restricted with allow = [root space]. Space membership is therefore the single access credential — no per-room invites.

  • Authoritative room set = the live hierarchy. DoseBot's scope resolver pages through /_matrix/client/v1/rooms/{rootSpaceId}/hierarchy. The MATRIX_ALLOWED_ROOM_IDS env is a degraded-mode fallback used only when that fetch fails — it must be non-empty in production (startup assertion) but does not gate anything while the hierarchy is reachable, and it doesn't need to enumerate every room.
  • The join-rule gate self-heals. Each reconcile cycle compares every room's actual join rule to the expected one; with MATRIX_GATE_ENFORCE=true (production since 2026-07-04) drifted rules are rewritten automatically. Watch for Join-rule gate check log lines: {"enforce":true,"compliant":37,"drifted":0} is the healthy signature.
  • The old approach is dead. A force-join cron (space-membership-sync-job) used to stuff users into rooms; it's retired and frozen under matrix/archive/. Don't resurrect it.

DoseBot internals — the 5-minute mental model

DoseBot is Matrix-only — the legacy Discord runtime (bot.ts, command-system.ts, the discord.js dependency) has been deleted; commands/*.ts now hold only platform-neutral command definitions consumed by src/core/command-registry.ts. matrix/runtime.ts is a thin ~435-line orchestrator: it wires the Matrix client and starts the loops, delegating env parsing to matrix/runtime-config.ts (RuntimeConfig, including Fallback Allowlist parsing). Interesting modules under src/:

admin/reconciler.ts

A ~380-line scheduler, not the convergence logic itself. Reads Admin State (persisted approvals/bans/admins/dedup markers) as intent and runs each Enforcement Pass in turn against it, on a timer and after admin commands.

Enforcement Pass modules

join-rule-gate.ts · ban-enforcement.ts · approval-enforcement.ts · admin-sync.ts · user-removal.ts · private-invite-revocation.ts, plus shared helpers in reconcile-support.ts. Each converges one aspect of live room state through its own narrowed client slice.

admin/scope-resolver.ts

Resolves the room Scope from the live hierarchy (source:'hierarchy'), falling back to the env allowlist (source:'fallback') only on fetch failure. Everything downstream — reconciler, map, commands — operates on this snapshot.

admin/admin-command.ts + admin-subcommands.ts

admin-command.ts is a ~90-line dispatcher over a 19-row registry in admin-subcommands.ts; help text and usage strings derive from the registry rows instead of being hand-written per command.

Send Seam + Onboarding Flow

matrix/matrix-send-client.ts defines MatrixSendClient — the one interface + structural adapter every onboarding-side Matrix call routes through (a fake adapter stands in for tests). matrix/onboarding-flow.ts's OnboardingFlow owns the newcomer path end to end — claim-greeting dedup → greeting → Scope Map DM — behind that seam; runtime event handlers just delegate to it.

matrix/account-data-state-store.ts

One generic AccountDataStateStore backs both persisted stores: Admin State (dev.dosebot.admin.v1) and onboarding dedup state (dev.dosebot.space_onboarding.v1). The keys are intentionally kept separate — see docs/adr/0001-greeting-dedup-separate-account-data-key.md — because cross-process writes are last-write-wins.

Working on DoseBot: run inside services/dosebot/npm test (tsx, 251 tests, no build needed), npx tsc --noEmit, then npm run build and commit the regenerated dist/. Bot account is @dosebotmx:josiekins.xyz. Read services/dosebot/AGENTS.md before nontrivial changes.

Ops automation

  • Health probe every 10 minmatrix-health.yml hits the public endpoints (Synapse versions, MAS, Cinny, discovery, the /matrix-info redirect). On failure it opens a deduplicated GitHub issue in this repo; with recovery enabled it can restart the failed service using the scoped Railway project token.
  • Emoji pack sync scheduled + on-demand — runs as both a GitHub workflow and a Railway job, authenticated as @maintenance-bot via MATRIX_EMOTE_SYNC_TOKEN.
  • Live validation suitematrix/validation/run.sh live-health | live-auth | live-smoke probes production read-only; the smoke test is also part of the josiekins deploy workflow.
  • Server map drift manual only — josiekins' npm run matrix:map:check (needs the maintenance-bot token) verifies the /matrix-info server map against the live hierarchy. It is NOT wired into CI; run it after changing the space layout.

Dev quickstart & sharp edges

CommandWhat it does
npm ci && npm ci --prefix services/dosebotinstall root tooling + bot deps
npm run checkfull pre-push gate: contracts + bot tests + build
npm run matrix:validateops contracts + duplicate-script guard
npm run matrix:scripts:syncre-copy canonical maintenance scripts over the per-service duplicates
npm run dosebot:test / dosebot:buildbot tests / tsc build from the repo root
npm run railway:statusread-only production status summary

Sharp edges that bite newcomers:

  • Forgetting to rebuild + commit services/dosebot/dist/ — CI's dist-sync check will fail your push.
  • Editing the copy of a maintenance script in emoji-pack-sync-job-service/ — the guard fails; edit the canonical one in synapse-service/ and sync.
  • Treating MATRIX_ALLOWED_ROOM_IDS as the room allowlist — it's fallback-only; the hierarchy is authoritative.
  • Changing Cinny's nginx conf or josiekins' discovery contract without checking the other side of the seam.
  • Running production-mutating Railway/Vercel commands without being explicitly asked — read-only inspection is fine, mutations are not.
  • Vocabulary drift — "map", "matrix/", "allowlist", and "sync job" are all overloaded; CONTEXT.md at the repo root is the tiebreaker.

developer explainer · sources: AGENTS.md, ARCHITECTURE.md, matrix/SKILL.md, CONTEXT.md, services/dosebot/AGENTS.md · no secrets or tokens on this page