Architecture
Shape
Caddy (TLS, routing by host)
┌──────────┬──────────┬──────────┬──────────┬──────────┬─────────┐
│ │ │ │ │ │ │
home id pages apps env … ai mail
(<d>) (id.<d>) (pages.<d>) (apps.<d>) (env.<d>) (ai.<d>) (mail.<d>)
│ │ │ │ │ │ │
└──────────┴──────────┴────┬─────┴──────────┴──────────┴─────────┘
│
PostgreSQL (one database per service)
│
xvii-node (root agent on every server) ── Docker, Caddy, systemd
- Every service is one Go binary: API, embedded panel, background jobs, MCP tools. They listen on
localhost; Caddy on the server terminates TLS and routes by host.
id signs people and programs in; every other service trusts it.
- PostgreSQL holds platform data: one cluster per instance, one database and one role per service.
xvii-node runs on every server as root. On the control-plane server it installs, updates and
restarts the platform services and writes Caddy's config; on other servers it runs apps workloads.
One agent, two jobs: platform services are "system workloads" of the same machinery.
Domains
An instance has one base domain <d>. Every service gets a host under it:
| Host |
Served by |
<d> |
home: dashboard, instance admin, /install.sh, /releases/* |
id.<d> |
id: sign-in, account, teams, OIDC (/.well-known/openid-configuration) |
<svc>.<d> |
that service's panel and API (/api/v1) and MCP endpoint (/mcp) |
*.pages.<d> |
pages sites (by deploy id or alias) |
*.apps.<d> |
apps endpoints (a DNS record per app, pointing at its node) |
*.tunnel.<d> |
tunnels |
*.hooks.<d> |
webhook endpoints |
mx.<d>, mail.<d> |
mail: MX host and webmail |
User content (*.pages.<d>, *.apps.<d>, *.tunnel.<d>) can be moved to a separate registrable domain
(content_domain in instance settings) so that user sites are not same-site with panels. Until it is set,
every API that accepts cookies checks Sec-Fetch-Site / Origin (see conventions/api.md).
TLS: a wildcard certificate per service zone through DNS-01 when the instance has a DNS provider token
(Cloudflare first), otherwise on-demand certificates over HTTP-01, each name approved by the owning
service through an ask endpoint. Custom domains always go on-demand, approved after a TXT check.
Identity and access
Details: services/id.md, decisions/0005-rbac.md.
- People sign in at
id.<d> with passkeys (WebAuthn RP ID = <d>, so one passkey works for the
whole instance). Recovery: one-time sign-in links from a superuser or from xvii-node recover.
- Panels are OIDC clients of
id (authorization code + PKCE). Each panel keeps its own host-only
session cookie; there is no cookie on .<d>. Signing in once at id makes every other panel a redirect.
- The CLI uses the OAuth device flow (RFC 8628) against
id and keeps a refresh token per instance.
- CI and scripts use API tokens (
xv_…), created in id for a user or a team, with a scope and an
optional restriction to services, a project and an expiry. GitLab and GitHub CI can skip stored tokens:
they send their OIDC job token and id exchanges it per a trust rule (repository, branch).
- Services validate access tokens (JWT, 15 minutes, signed by
id, keys at JWKS) locally, and
introspect API tokens at id with a 60 s cache. Service-to-service calls use client credentials.
- Authorization is RBAC on a hierarchy: instance → team → project → environment → resource. Team
roles: owner, admin, developer, viewer. Instance roles: superuser, user. Explicit grants share
resources across teams (a node, an AI quota). The access token carries instance role and team roles;
services decide on resources they own.
Hierarchy
instance
└── team (people with roles; quotas; personal team per user)
└── project (lives in id; every service attaches resources to it)
└── environment (production, preview, custom; protection rules)
└── resource (pages site, app, database, secret set, monitor, error project, …)
id owns teams, projects and environments, so home can show one project across services, and a
secret set in env, an app in apps and an error project in errors refer to the same project id.
Data
Details: decisions/0004-postgresql.md.
- PostgreSQL, installed by
xvii server install on the control-plane server. One database and one role
per service; a service never reads another's database, it calls the API.
- Go access through
pgx; queries in SQL (sqlc), migrations embedded in the binary and applied on
start (kit/db).
- Files that are not rows (pages sites, build caches, mail blobs, backups staging) live under
/var/lib/xvii/<service>/. Large blobs can go to S3-compatible storage where a service supports it.
- High-volume data (analytics events, errors, AI usage, metrics, logs) uses time-partitioned tables with
retention, plus rollups for dashboards.
Communication
- Synchronous: HTTP/JSON APIs under
/api/v1, the same ones the CLI uses. Internal calls go over
localhost on the control plane, authenticated with service tokens.
- Events: each service writes events into an outbox table in the same transaction as the change, and
a worker delivers them to
notify (POST /api/v1/events) with retries. notify stores them (the
instance activity feed home shows), fans them out to channels per subscription, and calls registered
webhooks. No message broker until volume demands one.
- Activity log: every change through any API is an audit event (actor, action, target, team,
project, IP), kept by the service and sent as an event.
- Search: each service serves
GET /api/v1/search?q= for its resources; home's ⌘K fans out.
- Nodes:
xvii-node keeps one outbound WebSocket to the control plane (apps.<d>/agent): desired
state in, status, metrics and logs out. No inbound port on a node; nodes may sit behind NAT.
Panels
Details: conventions/panel.md, services/ui.md.
Every service has its own panel on its own host, built from the shared shadcn registry @xvii so they
look and behave as one product: the same shell (sidebar, service switcher, team switcher, account
menu), the same tables, dialogs, empty states, i18n (en, ru) and formatting. home at <d> is the
front door: overview of the current team, projects across services, the activity feed, global search,
and instance administration for superusers.
Installation and updates
Details: services/cli.md, services/node.md,
decisions/0010-node-agent-manages-platform.md.
xvii server install (in the CLI) connects to a fresh Ubuntu/Debian server over SSH, or runs on it,
and walks through a wizard: domain, DNS provider token, which services, e-mail for ACME. It installs
PostgreSQL, Caddy and xvii-node; xvii-node installs the chosen services and prints the first
sign-in link.
- Releases are versioned per service. An instance follows a channel (
stable, edge); home shows
available updates and asks xvii-node to apply them (download, verify checksum and signature,
migrate, restart, health check, roll back on failure).
xvii server add-node (or the join command shown in apps) adds a workload server.
Repositories
All in the xvii-network group of the project's GitLab, push-mirrored to github.com/xvii-network. Go
module paths are go.xvii.network/<repo> (served by go).
| Repo |
Contents |
docs |
this: specs, decisions, and the documentation site with its service docsd |
ci |
GitLab CI templates (go.yml, panel.yml, pages.yml) |
kit |
shared Go packages |
ui |
shadcn registry @xvii |
cli |
xvii |
node |
xvii-node and its protocol package (imported by apps and home) |
id, home, pages, apps, env, notify, backups, tunnel, hooks, status, errors, analytics, ai, mail, share, config, go |
one service each |
Local workspace: every repository side by side in one folder, with a go.work at the top that puts the Go
modules together (CI builds against tags instead).