Glyphoxa supports multi-tenant SaaS deployments through a gateway/worker architecture. This document covers the tenant model, admin API, session orchestration, usage tracking, and campaign export/import.
For single-process self-hosted deployments, see Deployment β --mode=full requires no tenant configuration.
Binary Modes
The Glyphoxa binary supports four modes via the --mode flag:
| Mode | Description | Use case |
|---|---|---|
full (default) | Single-process. Gateway and worker in one binary. No admin API. Config from YAML. | Self-hosted, open-source |
gateway | Session orchestrator. Manages Discord bots, routes sessions to workers via gRPC. Serves admin API. | SaaS control plane |
worker | Voice pipeline executor. Receives session commands from gateway. Connects directly to Discord voice. | SaaS data plane |
mcp-gateway | Shared MCP tool server. Hosts stateless tools (dice, rules) and proxies external MCP servers over HTTP. | SaaS tool layer |
In --mode=full, the gateway and worker contracts are satisfied by in-process function calls (internal/gateway/local/), so the full pipeline runs identically to distributed mode without network overhead.
Tenant Model
A tenant represents a paying customer (one license). Each tenant owns:
- A license tier (
sharedordedicated) - A Discord bot token (one bot per tenant)
- A set of guild IDs the bot is allowed to join
- A monthly session hour quota
- One or more campaigns (game worlds with NPCs, entities, and session history)
Data Isolation
| Tier | Database | Infrastructure |
|---|---|---|
| Shared | Schema-per-tenant in a shared PostgreSQL instance. Each tenant gets its own schema with full table set. DROP SCHEMA CASCADE for clean offboarding. | Shared gateway + shared worker node pool |
| Dedicated | Dedicated PostgreSQL instance | Dedicated gateway + dedicated worker node pool |
Schema names are validated at construction (^[a-z][a-z0-9_]{0,62}$) and sanitized with pgx.Identifier β SQL injection through schema names is impossible by construction.
TenantContext
Every request in the system carries a TenantContext containing tenant_id and campaign_id. This context:
- Scopes all database queries to the correct schema
- Labels all metrics and traces with
tenant_idandcampaign_id - Determines which bot token to use for Discord connections
- Enforces quota limits on session start
Admin API
The gateway exposes an internal HTTP API on a separate port (default :8081), protected by API key authentication and NetworkPolicy.
Authentication
All requests must include the Authorization: Bearer <api-key> header. The API key is set via the GLYPHOXA_ADMIN_KEY environment variable.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /tenants | Create a new tenant |
GET | /tenants | List all tenants |
GET | /tenants/{id} | Get tenant details |
PUT | /tenants/{id} | Update tenant (tier, bot token, guilds) |
DELETE | /tenants/{id} | Delete tenant and all data |
Create Tenant
POST /tenants
{
"id": "acme-corp",
"license_tier": "shared",
"bot_token": "MTIzNDU2Nzg5..."
}
Update Tenant
PUT /tenants/acme-corp
{
"license_tier": "dedicated",
"guild_ids": ["123456789", "987654321"]
}
Session Orchestration
The sessionorch.Orchestrator manages distributed session lifecycle with three states:
pending β active β ended
Constraint Enforcement
On ValidateAndCreate, the orchestrator checks:
- Concurrent session limit β enforced by the license tier (database-level unique constraints prevent races)
- Quota guard β
usage.QuotaGuardwraps the orchestrator and rejects sessions when the tenantβs monthly session hours are exhausted
Heartbeat & Zombie Cleanup
Workers send periodic heartbeats to the gateway via gRPC. If a worker dies:
- The heartbeat stops arriving
CleanupZombies(timeout)transitions stale sessions (no heartbeat for >90s) toended- A new session can then be started
Implementations
| Implementation | Backend | Used by |
|---|---|---|
PostgresOrchestrator | PostgreSQL sessions table with exclusion constraints | --mode=gateway |
MemoryOrchestrator | In-memory map | --mode=full |
Usage & Quota Tracking
The usage.Store tracks per-tenant resource consumption per billing period (monthly):
| Metric | Description |
|---|---|
session_hours | Total hours of active voice sessions |
llm_tokens | LLM tokens consumed |
stt_seconds | Speech-to-text audio seconds processed |
tts_chars | Text-to-speech characters synthesised |
Quota Enforcement
QuotaGuard wraps the session orchestrator. Before creating a session, it calls Store.CheckQuota(). If the tenant has reached their monthly_session_hours limit, the session is rejected with ErrQuotaExceeded.
gRPC Contract
Gateway and worker communicate via two gRPC services defined in proto/glyphoxa/v1/session.proto:
SessionWorkerService (worker exposes, gateway calls)
| RPC | Direction | Purpose |
|---|---|---|
StartSession | gateway β worker | Launch voice pipeline for a session |
StopSession | gateway β worker | Tear down a running session |
GetStatus | gateway β worker | Query active session statuses |
SessionGatewayService (gateway exposes, worker calls)
| RPC | Direction | Purpose |
|---|---|---|
ReportState | worker β gateway | Report session state transitions |
Heartbeat | worker β gateway | Periodic liveness signal |
The gRPC client (grpctransport.Client) wraps all calls with a circuit breaker to prevent cascading failures when a worker becomes unreachable.
In --mode=full, these contracts are satisfied by local.Client and local.Callback which make direct function calls with no serialisation overhead.
Bot Management
BotManager manages per-tenant Discord bot connections:
- Each tenant has exactly one bot (one token)
AddBot/RemoveBotmanage bot lifecycleRouteEventdispatches Discord events to the correct tenantβs bot- Thread-safe: all operations are guarded by
sync.Mutex
The gateway starts a bot for each tenant on startup and adds/removes bots when tenants are created or deleted via the admin API.
Campaign Export & Import
Campaigns can be exported as .tar.gz archives and imported into another tenant or environment.
Archive Structure
campaign-export.tar.gz
βββ metadata.json # CampaignID, TenantID, LicenseTier, ExportedAt, Version
βββ npcs/
β βββ grimjaw.yaml # NPC definition
β βββ elara.yaml
βββ knowledge-graph.json # Entities and relationships with provenance
βββ sessions/
βββ session-001.txt # Session transcript
βββ session-002.txt
Usage
Export and import are available through the pkg/memory/export package:
WriteTarGz(w io.Writer, data ExportData) errorβ creates archiveReadTarGz(r io.Reader) (*ImportData, error)β reads and validates archive
L2 semantic chunks (vector embeddings) are included only for Dedicated tier exports, as they are large and can be regenerated.
Key Source Files
| File | Description |
|---|---|
cmd/glyphoxa/main.go | Mode flag parsing and dispatch |
internal/gateway/admin.go | Admin API HTTP handlers |
internal/gateway/botmanager.go | Per-tenant bot lifecycle |
internal/gateway/contract.go | WorkerClient and GatewayCallback interfaces |
internal/gateway/sessionorch/orchestrator.go | Session lifecycle and constraints |
internal/gateway/usage/quota_guard.go | Quota enforcement wrapper |
internal/gateway/grpctransport/client.go | gRPC WorkerClient with circuit breaker |
internal/gateway/local/client.go | In-process WorkerClient for full mode |
internal/session/runtime.go | Voice pipeline lifecycle |
internal/session/worker_handler.go | gRPC handler managing Runtime instances |
pkg/memory/export/ | Campaign archive read/write |
proto/glyphoxa/v1/session.proto | gRPC service and message definitions |
See also: Architecture Β· Deployment Β· Observability Β· Configuration