Admin Dashboard

Platform administration dashboard for managing organizations, VPS provisioning, health monitoring, billing, and infrastructure.

The nest dashboard (apps/nest/) is a standalone Nuxt 3 application for platform-level administration. It is separate from the tenant-facing web app and provides tools for managing organizations, VPS infrastructure, billing, and platform health.

Internal tool

The admin dashboard is an internal tool and is not exposed to end users. It requires the platform-admin role to access.

Architecture

AspectDetail
FrameworkNuxt 3 (CSR-only, ssr: false)
UI LayerExtends packages/ui for shared components
BackendConvex (separate deployment: apps/nest-api)
InfrastructureHetzner Cloud VPS via cloud-init provisioning
AuthBetterAuth with platformAdmins table (admin/superadmin roles)
Port3001 (separate from web app on 3000)

The admin app connects to its own Convex backend (apps/nest-api), which is a separate deployment from the tenant app (apps/api). The control plane manages VPS provisioning, billing, health monitoring, and organization lifecycle.

Authentication Flow

Admin navigates to /loginEmail + password form
BetterAuthAuth proxy forwards to nest-api Convex
Session createdJWT token issued
platformAdmins table checkedQuery: isPlatformAdmin
Dashboard access granted

Auth Proxy Pattern

The admin app proxies authentication requests through a same-origin server route (server/api/auth/[...].ts) to avoid CORS issues. This route forwards requests to the Convex site URL, strips problematic headers (Host, Connection), and handles Set-Cookie domain adjustments for local development.

Browser → /api/auth/* → Nuxt server route → Convex site URL → response proxied back

Token Management

The lib/convex-auth.ts module manages JWT tokens for Convex:

  • Caches the current token to avoid unnecessary auth calls
  • Implements a 60-second refresh buffer (refreshes before expiry)
  • Provides getConvexAuthToken() for the Convex client's auth callback

Pages

RoutePageMiddlewarePurpose
/index.vueauthDashboard overview: pending orgs, health alerts, revenue summary
/loginlogin.vueAdmin sign-in (email + password)
/unauthorizedunauthorized.vueShown when user lacks platform-admin role
/organizationsorganizations/index.vueauthOrganization listing with status, tier, VPS health
/organizations/[id]organizations/[id].vueauthOrganization detail: suspend/resume/delete/resize actions
/organizations/neworganizations/new.vueauthCreate new organization with tier and region selection
/vpsesvpses/index.vueauthVPS infrastructure dashboard: all instances, health status
/billingbilling/index.vueauthRevenue dashboard: MRR, total revenue, open/overdue invoices
/tierstiers/index.vueauthPricing tier configuration and management
/adminsadmins.vueauthPlatform admin management (add/remove admins)
/auditaudit.vueauthAudit log viewer for all admin actions
/waitlistwaitlist.vueauthSignup approval queue: approve/reject pending organizations
/signupsignup.vuePublic signup page: tier/region selection, Stripe payment
/pendingpending.vueProvisioning status page (shown to users after signup)
/setupsetup.vueInitial platform setup wizard

Middleware

Two middleware layers protect admin routes:

  1. auth.ts — redirects unauthenticated users to /login (with redirect query param)
  2. platform-admin.ts — queries the platformAdmins table and redirects non-admins to /unauthorized

Organization Lifecycle

Organizations follow a state machine managed by the control plane:

pendingCreated via signup or admin, awaiting payment/approval
provisioningHetzner API creating server
deployingWaiting for Docker services to start
activeInstance accessible, routing enabled
suspended / cancelled / deletedAdmin action or subscription change

States

StateDescription
pendingCreated via signup or admin. Awaiting payment confirmation or admin approval.
provisioningVPS being created on Hetzner Cloud via API.
deployingServer created, waiting for Docker Compose services to start (smoke test).
activeAll services running. Routing table entry active. Instance accessible.
suspendedAdmin-suspended or payment overdue. Routing disabled, VPS still running.
cancellingStripe subscription cancelled. Instance remains until end of billing period.
cancelledSubscription ended. Instance may still exist but routing is disabled.
deletingVPS deletion in progress via Hetzner API.
deletedVPS destroyed. Routing entry removed.
errorProvisioning or operation failed. Admin can retry or investigate.

Admin Actions

  • Suspend — Disables routing (org becomes inaccessible), VPS remains running
  • Resume — Re-enables routing for a suspended org
  • Change tier — Updates tier and queues a VPS resize job if server type differs
  • Delete — Queues VPS deletion job, disables routing immediately
  • Approve signup — Triggers VPS provisioning for a pending organization
  • Reject signup — Sets organization to cancelled status

VPS Provisioning

The control plane manages VPS infrastructure through a job queue system.

Job Types

TypeDescription
createProvision a new VPS: Hetzner API → cloud-init → Docker Compose → smoke test
resizeChange server type: power off → Hetzner resize API → power on
deleteDestroy VPS: Hetzner delete API → remove routing entry
redeployUpdate running services: self-update endpoint or Hetzner reboot fallback

Jobs are queued in the provisioningJobs table and processed every 1 minute by a cron job.

Create Flow

  1. Admin approves signup or payment is confirmed via Stripe webhook
  2. Provisioning job queued with type: 'create'
  3. Secrets generated: instanceSecret, convexAdminKey, mtaApiKey, mtaWebhookSecret
  4. VPS record created in database
  5. Hetzner API called to create server with cloud-init userdata
  6. Cloud-init installs Docker, writes .env and docker-compose.yml, starts all services
  7. Smoke test polls Convex (:3210) and Web (:3000) — up to 5 minutes
  8. Routing table entry created (slug → IPv4)
  9. Organization marked as active

Available Regions

RegionLocationContinent
fsn1Falkenstein, GermanyEU
nbg1Nuremberg, Germany (default)EU
hel1Helsinki, FinlandEU
ashAshburn, VA, USAUS
hilHillsboro, OR, USAUS
sinSingaporeAsia

Per-VPS Stack

Each VPS runs six Docker services via Docker Compose:

ServicePurpose
convexSelf-hosted Convex backend (ports 3210/3211)
webNuxt web application (port 3000)
mtaMail Transfer Agent (port 3100 API, port 25 SMTP)
redisQueue and cache layer
clamavAntivirus scanning for email attachments
convex-deployOne-shot schema deployment helper

Health Monitoring

The control plane runs automated health checks every 5 minutes against all running VPS instances.

Checked Services

ServiceEndpointPort
ConvexGET /version3210
WebGET /3000
MTAGET /health3100

Status Determination

StatusCondition
healthyAll three services responding
degradedConvex up + at least one other service up
unhealthyOnly one service responding
unreachableNo services responding

Alerting and Auto-Recovery

ThresholdConsecutive FailuresTimeAction
Alert3~15 minHealth alert created for admins
Auto-reboot6~30 minHetzner server reboot triggered via API

Health Alert Types

TypeDescription
unhealthyInstance has been failing health checks beyond the alert threshold
auto_rebootAutomatic reboot triggered due to prolonged failure
reboot_failedAuto-reboot attempt failed (Hetzner API error)
recoveredInstance returned to healthy status

Billing and Invoicing

Pricing Tiers

TierServer TypeSpecsMonthly Price
Startercx222 vCPU, 4 GB RAM, 40 GB disk€28
Growthcx324 vCPU, 8 GB RAM, 80 GB disk€60
Enterprisecx428 vCPU, 16 GB RAM, 160 GB disk€120

Stripe Integration

Each tier has a corresponding Stripe Price ID. When a user signs up:

  1. Stripe Customer created
  2. Stripe Subscription created with payment_behavior: 'default_incomplete'
  3. Client secret returned for Stripe Elements payment confirmation
  4. On invoice.paid webhook — organization provisioning is triggered automatically
  5. On invoice.payment_failed — organization marked as error, user can retry

Invoice Generation

Monthly invoices are generated automatically by a daily cron job:

  • Format: INV-YYYYMM-XXXX (e.g., INV-202603-0001)
  • Payment terms: 14 days from period start
  • Statuses: draftopenpaid / overduevoid
  • Overdue detection runs alongside invoice generation

Revenue Dashboard

The /billing page displays:

  • MRR — Sum of monthly tier prices for all active organizations
  • Total revenue — Sum of all paid invoices
  • Open invoices — Count and total amount
  • Overdue invoices — Count and total amount

Signup Flow

Self-Service (User-Initiated)

User visits /signupSelects tier and region
Stripe paymentSubscription created, payment confirmed
Organization created (pending)Stripe webhook triggers provisioning
VPS provisionedUser sees /pending page with progress
Instance activeUser redirected to their instance

Admin Manual

Admins can create organizations directly from /organizations/new:

  1. Enter organization name, slug, owner email
  2. Select tier and region
  3. Provisioning job queued immediately (no payment step)

Alternatively, admins can approve pending signups from the /waitlist page:

  1. Review pending organization details
  2. Optionally change the tier
  3. Approve — triggers VPS provisioning
  4. Reject — sets organization to cancelled with optional reason

Control Plane Backend (apps/nest-api)

The control plane is a separate Convex deployment managing all platform infrastructure.

Key Modules

ModulePurpose
organizations.tsOrganization CRUD, lifecycle transitions, signup approval
vpses.tsVPS instance queries and mutations
provisioning.tsJob processor: create, resize, delete, redeploy VPS operations
provisioningJobs.tsJob queue management (queued → running → succeeded/failed)
healthChecks.tsAutomated health monitoring and auto-recovery
billing.tsInvoice generation, MRR calculation, overdue detection
stripe.tsStripe subscription creation for self-service signup
tiers.tsPricing tier management and seeding
routing.tsRouting table management for Caddy reverse proxy
auditLogs.tsPlatform admin action audit trail
platformAdmins.tsAdmin user management (admin/superadmin roles)

Cron Jobs

JobIntervalFunction
Health checks5 minhealthChecks.checkAll
Process provisioning jobs1 minprovisioning.processJobs
Generate monthly invoices24 hoursbilling.generateMonthlyInvoices

Composables

ComposablePurpose
useAuth()BetterAuth session management: signInWithEmail(), signOut(), waitUntilReady(), refetch(). Exposes reactive status (pending/authenticated/unauthenticated/error).
useConvexQuery()Reactive Convex query wrapper. Returns { data, error, isLoading }. Deep-watches args for reactivity. Supports 'skip' pattern for conditional queries.
useConvexMutation()Convex mutation executor. Returns { mutate, isLoading, error }.
useConvex()Base composable returning the Convex client instance.

Query Usage Pattern

const { data: organizations, isLoading } = useConvexQuery(
  api.organizations.list,
  () => ({ status: selectedStatus.value })
)

The args factory function is deep-watched — when selectedStatus changes, the query automatically re-subscribes with new arguments.

Environment Variables

Admin Dashboard (apps/nest/.env)

VariableDescriptionDefault
NUXT_PUBLIC_CONVEX_URLConvex deployment URL for the nest-api deployment
NUXT_PUBLIC_CONVEX_SITE_URLConvex site URL for the nest-api deployment (used for auth proxy)
NUXT_PUBLIC_SITE_URLAdmin dashboard URLhttp://localhost:3001

Control Plane Backend (nest-api Convex dashboard)

VariableDescription
BETTER_AUTH_SECRETSession signing secret for admin auth
SITE_URLAdmin dashboard URL for auth redirects
HETZNER_API_TOKENHetzner Cloud API token for VPS provisioning
HETZNER_SSH_KEY_IDSComma-separated SSH key IDs for server access
STRIPE_SECRET_KEYStripe API secret key
STRIPE_WEBHOOK_SECRETStripe webhook signing secret
STRIPE_PRICE_STARTERStripe Price ID for Starter tier
STRIPE_PRICE_GROWTHStripe Price ID for Growth tier
STRIPE_PRICE_ENTERPRISEStripe Price ID for Enterprise tier

Security

The admin app is configured with strict security headers via nuxt-security:

  • CSP — strict script-src, object-src: 'none', Convex URL in connect-src
  • HSTS — enabled with long max-age
  • X-Frame-OptionsDENY (prevents embedding)
  • Referrer-Policystrict-origin-when-cross-origin

Key Files

Admin Dashboard (apps/nest/)

FilePurpose
nuxt.config.tsApp configuration, security headers, runtime config
app/lib/auth-client.tsBetterAuth client initialization
app/lib/convex-auth.tsJWT token caching and refresh
app/plugins/convex.client.tsConvex client initialization and auth wiring
server/api/auth/[...].tsAuth proxy server route
app/middleware/auth.tsAuthentication middleware
app/middleware/platform-admin.tsPlatform admin role middleware
app/layouts/default.vueSidebar layout with navigation

Control Plane Backend (apps/nest-api/)

FilePurpose
convex/schema.tsControl plane database schema (9 tables)
convex/provisioning.tsVPS create/resize/delete/redeploy + cloud-init generation
convex/healthChecks.tsHealth check logic, alerting, auto-reboot
convex/billing.tsInvoice generation, MRR calculation
convex/stripe.tsStripe subscription creation for signups
convex/organizations.tsOrganization CRUD and lifecycle management
convex/tiers.tsTier definitions and seeding
convex/crons.tsCron job definitions (health, provisioning, billing)
convex/lib/regions.tsHetzner region definitions (6 datacenters)

Infrastructure (infra/)

FilePurpose
pulumi/src/vps.tsPulumi component for Hetzner VPS provisioning
pulumi/src/cloudinit.tsCloud-init script generator
templates/docker-compose.vps.ymlPer-VPS Docker Compose template
templates/.env.vps.templatePer-VPS environment variable template
caddy/Caddy reverse proxy configuration