Appearance
Exto Console — Project Overview
What is it?
Exto Console is a full-stack SaaS control plane and management platform. It serves as the operational backbone for Exto's multi-tenant workflow product — enabling the Exto team to manage customer instances, provision tenants, handle billing, and giving customers a self-service portal to manage their own workspaces.
Architecture
The system has three main runnable components:
| Component | Path | Purpose |
|---|---|---|
| API Server | cmd/api/ | REST API backend (Go + Gin) |
| Background Worker | cmd/worker/ | Async job processor |
| Frontend SPA | web/ | Admin + customer portal (React + TypeScript) |
Data store: PostgreSQL Identity provider: Zitadel (OpenID Connect, user management)
Core Features
Instance Management
- Register and track infrastructure instances
- Health monitoring via heartbeat push + scheduled polling
- Track latency, uptime, CPU/memory/disk metrics
- Automatic degraded-status detection based on resource thresholds
Customer & Tenant Lifecycle
- Customer CRUD with plan/billing setup and status (
active/suspended/churned) - Tenant state machine:
requested → provisioning → active → suspended / archived - Auto-assignment of tenants to least-loaded instance
- 30-day grace period before purging churned customers
User Management
Three user types:
tenant_users— end-users of the productcustomer_portal_users— customer admins managing their own orginternal_users— Exto team with roles:platform_admin,ops_engineer,finance_admin
Invite flow, Zitadel-backed provisioning, and profile caching with lazy refresh.
Billing & Invoicing
- Usage event ingestion with idempotent handling
- Hourly aggregation across meters (
active_users,api_calls,storage_gb,workflow_executions, etc.) - Plan limits with soft/hard gates
- Invoice generation with PDF export
- Dunning workflow: emails at 3/7/14 days overdue → auto-suspend on day 15
- Credit ledger with atomic operations
- Per-tenant billing config overrides
Migration Orchestration
- 5-step pipeline:
copy-config → upgrade-config → copy-data → delete-config → delete-data - Dry-run enforcement before live execution
- Snapshot/restore rollback
- Concurrency limits (max 3 concurrent jobs, no cycles)
- Structured job logs and verification reports
Auth Configuration (Self-Service)
- SSO setup (Google/Azure OIDC per org)
- Password policy, MFA policy, session timeout config
- Managed through Zitadel Management API
Service Accounts
- Machine users with client secrets (shown once on creation)
- Secret rotation, deactivation, and token usage tracking
- Permission scoping per tenant + instance
Notifications & Webhooks
- In-app notifications with read/unread state
- Email dispatch (SendGrid/SMTP)
- Customer webhook registration with HMAC signing
- Notification templates with per-channel customization
Insights
- Tenant-level: DAU/MAU, workflow throughput, error rates
- Instance-level: health score, p50/p95/p99 latency, uptime
Audit Logging
- Immutable audit event trail, searchable and exportable
Background Worker Jobs
| Job | Interval | Purpose |
|---|---|---|
| Health Poller | 5 min | Polls instance /internal/health endpoints |
| Degraded Watcher | 60 sec | Evaluates resource thresholds, updates instance status |
| User Provisioner | 5 min | Syncs users to instances |
| Tenant Provisioner | 2 min | Activates pending tenant-instance bindings |
| Usage Aggregation | Hourly | Rolls up usage events with watermark tracking |
| Dunning | 6 hr | Sends overdue emails, auto-suspends on day 15 |
| Migration Runner | 30 sec | Executes queued migration jobs |
| Customer Purge | 6 hr | Deletes churned customers after 30-day grace period |
Tech Stack
| Layer | Technologies |
|---|---|
| Backend | Go 1.25, Gin, pgx (PostgreSQL driver), goose (migrations), lestrrat-go/jwx (JWT/JWKS) |
| Frontend | React 19, TypeScript 5.8, Vite 6, Tailwind CSS 4, Zustand, React Query, React Router 7 |
| Auth | Zitadel (OIDC, Management API, PKCE flow) |
| SendGrid / SMTP |
Portal UIs
Internal Admin Portal
Role-based dashboards for platform_admin, ops_engineer, and finance_admin:
- Instance health grid and connection management
- Customer + tenant management with detail views
- Migration job management
- Audit log search and export
- Finance pages (invoices, payments, credits)
Customer Self-Service Portal
- Tenant viewing and creation
- Billing/usage dashboard and invoice download
- Organization settings (billing contact, timezone)
- Portal user management
- Auth config (SSO, MFA, password policy)
- Service account management
- Webhook registration
Data Model Hierarchy
Customer
├── Tenant [1..N]
│ ├── TenantInstanceBinding → Instance
│ ├── TenantAuthPolicy
│ ├── TenantBillingConfig
│ └── TenantUser [0..N]
├── CustomerPortalUser [0..N]
├── ServiceAccount [0..N]
├── Invoice [0..N]
├── Payment [0..N]
├── CustomerCredit (ledger)
└── CustomerWebhook [0..N]
Instance
└── InstanceConnection (DB/API credentials)
Job (migration)
├── JobLog []
└── JobArtifact []Project Structure
console/
├── cmd/
│ ├── api/ # API server entry point + dependency wiring
│ └── worker/ # Background worker entry point
├── internal/
│ ├── config/ # Environment config loading
│ ├── db/ # PostgreSQL connection + goose migrations
│ ├── model/ # Data models (instance, customer, user, billing, etc.)
│ ├── handler/ # HTTP request handlers (organized by domain)
│ ├── worker/ # Background job implementations
│ ├── auth/ # JWT middleware + customer scope enforcement
│ ├── zitadel/ # Zitadel Management API client
│ ├── notify/ # Notification dispatcher (email, webhook, in-app)
│ └── audit/ # Audit event logger
├── web/ # React SPA frontend
│ └── src/
│ ├── auth/ # OIDC/PKCE auth flow
│ ├── api/ # Typed API client layer
│ ├── pages/ # Admin, finance, and portal page components
│ └── components/
├── scripts/
│ └── bootstrap/ # Zitadel org/project setup + DB seeding
├── docs/ # Project documentation
├── Makefile
└── .env.exampleConfiguration
Key environment variables (see .env.example):
env
API_ADDR=:8000
DATABASE_URL=postgres://console:password@localhost:5432/console?sslmode=disable
ZITADEL_ISSUER=http://localhost:8080
ZITADEL_API_URL=http://localhost:8080
ZITADEL_PAT=<personal-access-token>
ZITADEL_ADMIN_ORG_ID=<org-id>
EXTOID_PROJECT_ID=<project-id>
CONSOLE_SERVICE_CLIENT_ID=<id>
CONSOLE_SERVICE_CLIENT_SECRET=<secret>
ZITADEL_WEBHOOK_SECRET=<signing-key-from-bootstrap>
HEALTH_POLL_INTERVAL_SECS=300
EMAIL_PROVIDER=sendgrid
EMAIL_API_KEY=<sendgrid-api-key>
EMAIL_FROM_ADDR=noreply@exto360.comRunning the Project
bash
# Backend
make api # Start API server on :8000
make worker # Start background worker
# Frontend
cd web
npm install
npm run dev # Vite dev server
npm run build # Production buildAPI Overview
The REST API is versioned under /api/v1/. Key route groups:
| Group | Base Path | Auth |
|---|---|---|
| Instances | /api/v1/instances | JWT (admin roles) |
| Customers | /api/v1/customers | JWT (admin or customer-scoped) |
| Tenants | /api/v1/tenants | JWT |
| Users | /api/v1/tenants/:id/users | JWT |
| Billing | /api/v1/customers/:id/... | JWT (customer-scoped) |
| Migrations | /api/v1/migrations, /api/v1/jobs | JWT |
| Notifications | /api/v1/notifications | JWT |
| Audit | /api/v1/audit | JWT (admin) |
| Server (exto-go) | /api/v1/server/instances/:id/... | Instance token |
| Agent | /api/v1/agent/instances/:id/... | Instance token |
| Zitadel events | /internal/zitadel-events | HMAC signature |

