Platform Hesabi ·DarDev Team · 7 min read

Self-hosted license control plane: Go, SQLite, and instance lifecycle

DarDev's license control plane is a standalone Go service with SQLite on dardev-vps—activate, heartbeat, suspend, and revoke for Hesabi self-hosted and future products.

License control plane diagram with Hesabi self-hosted instances heartbeating to Go API

DarDev License Control Plane is a standalone Go service with SQLite on dardev-vps that governs self-hosted product instances—Hesabi today, additional solutions like 20.tn later. It replaces the retired Node Prisma license server with platform/license: activate once, heartbeat on schedule, grace and suspend for payment or policy issues, revoke for hard termination. Cloud editions like hesabi.tn use the ops API to issue keys; self-hosted editions never receive ops keys—only instance tokens and signed license JWTs.

Production runs at licence.hesabi.dardev.net on path /opt/dardev-license with Docker Compose; target hostname licence.dardev.net aliases during migration. DarDev Ops deploys and monitors health—it does not operate day-to-day key issuance, which stays in Hesabi cloud superadmin. multi-product-saas-one-core explains how one codebase spans cloud and self-hosted editions.

Architecture decisions

  • Backend: Go net/http aligned with platform bus patterns—single static binary
  • Database: SQLite only with modernc.org/sqlite pure Go driver and WAL mode—no Postgres path
  • Admin UI: HTML and CSS server-rendered templates—no React SPA in license service
  • Scale: hundreds of self-hosted instances and ~1K internal ops users—SQLite is correct
  • Products interpret entitlements—server stores plan, features JSON, maxUsers, maxOrganizations

The control plane is Domain C infrastructure in DarDev Ops architecture—separate from company CRM, Listmonk, and mail stacks. Hesabi maps server fields to accounting, payroll, and module gates in its own codebase; no Hesabi business logic ships inside the Go binary.

Repository layout

Go module lives at platform/license with cmd/license-server main, internal/api handlers, internal/store SQLite repositories, internal/jwt RS256 sign and verify, internal/sweep background grace job, and web/static plus web/templates for admin. Migrations embed SQL run on startup when AUTO_MIGRATE=true. Contracts in packages/contracts/schemas/ define license-instance-v1 and license-activation-v1 JSON Schema for ops-validate.

Ops solution manifest hesabi_license targets rename to dardev_license. Fabric tasks dardev-license.deploy push images and compose to dardev-vps. E2E scripts test_dardev_license_e2e.py and test_license_server_e2e.py gate API parity against the retired Node server.

Data model

Single license.db file on persistent volume. Core tables: solutions registry with solution_id scope; license_customers; license_activations with hashed keys and plan limits; license_instances with fingerprint, token hash, status, heartbeat timestamps; license_events audit trail; instance_metrics latest heartbeat JSON; activate_rate_limits per IP; remote_config per solution for read-only flags and maintenance banners.

Status enum: ACTIVE, GRACE, SUSPENDED, REVOKED. Background sweep goroutine every fifteen minutes transitions missed heartbeats GRACE then SUSPENDED. The license server does not store Stripe billing, Hesabi feature matrix source code, or product user accounts—only opaque entitlements payloads.

Instance-facing API

  • GET /health — liveness for Ops connector and load balancers
  • POST /v1/instances/activate — body activationKey, fingerprint, optional solutionId inferred from key prefix
  • POST /v1/instances/{id}/heartbeat — bearer instance token; body fingerprint, version, metrics

Response shape: instanceToken, licenseJwt, config, configVersion, status, graceUntil. Hesabi self-hosted sets LICENSE_SERVER_URL to the licence host and DEPLOYMENT_MODE=self-hosted. TypeScript client in Hesabi repo—self-hosted-instance-guard.ts enforces JWT plus status on reads and writes; heartbeat scheduler posts orgCount, userCount, invoiceCount, appVersion metrics.

Ops API and admin dashboard

Ops endpoints require Authorization Bearer with LICENSE_OPS_API_KEY scoped to one solution_id or platform master. Issue activation POST /v1/solutions/{solutionId}/activations; list and patch instances; fetch events; read and patch remote config for maintenance banner in French, Arabic, and English.

Admin HTML at /admin/ on same host as API—login with platform master ops key, solutions list with instance counts, issue key form showing one-time activation key, instance detail with suspend and renew actions. Vanilla fetch for inline actions without React build step. Product cloud superadmin remains primary key issuance surface for customer-facing workflows.

Activation key format

Keys follow {solutionId}-{env}-{random}—for example hesabi-live- prefix segment allows activate handler to validate solution without extra client configuration. Cloud Hesabi superadmin proxies ops API with LICENSE_SERVER_OPS_URL and solution-scoped key env vars set in deployment config only—not in documentation or news articles.

Auth model

  • Platform master — license admin HTML and DarDev Ops diagnostics, all solutions
  • Solution ops key — hesabi.tn or future product cloud env, single solution_id
  • Instance token — one self-hosted deployment
  • License JWT — runtime entitlements inside self-hosted app

JWT uses RS256 key pair—private key on server volume, public key in product env or baked self-hosted image. Passwords and ops keys compared with crypto/subtle constant-time; activation keys hashed with server-side pepper never published.

Deploy and operations

Host dardev-vps 51.75.205.103, compose project dardev-license, bind 127.0.0.1:3010 proxied by host nginx TLS on 443. Backups: nightly copy license.db and WAL snapshot off-VPS. Observability: structured slog logs for activate failures, heartbeat lag, sweep transitions; Ops connector health_url.

Migration phases P0 boundaries through P4 Hesabi production cutover are complete. P5 registers second solution_id such as 20tn with scoped ops key and E2E. Node to Go data migration added solution_id=hesabi to all legacy rows so existing instances kept token hashes without re-activation.

Cloud vs self-hosted roles

Cloud SaaS hesabi.tn uses ops API only to issue self-hosted keys—no heartbeat required per cloud tenant on license plane. Self-hosted requires activate plus heartbeat plus JWT-driven entitlements. Unified cloud billing on license plane is explicitly out of scope; Stripe stays in product until a future roadmap.

deployed-realities-explained honesty applies: licence host is production infrastructure we operate—not a slide-deck diagram. Hesabi Tunisia-only; license plane does not change geographic product availability.

Migration from Node license server

The retired Node service at /opt/hesabi-license used Prisma over SQLite with the same activate and heartbeat contract Hesabi self-hosted clients already implemented. P4 cutover deployed Go at /opt/dardev-license without requiring customers to re-enter keys: a one-time migrate-from-legacy command copied rows, added solution_id=hesabi, and preserved instance token hashes so heartbeats continued uninterrupted.

Hesabi repo removed the embedded license-server binary; only TypeScript HTTP clients remain—license-server-admin.ts for cloud superadmin proxy and self-hosted heartbeat scheduler. This boundary keeps product releases independent from license plane deploys Fabric pushes on dardev-vps.

Remote config and maintenance banners

remote_config table stores per-solution JSON: readOnly flag, maintenance banner strings in multiple languages, minimum app version, and configVersion incremented on each PATCH. Heartbeat responses include config and configVersion so Hesabi applies banner text without separate polling endpoint. Suspend workflows often pair status SUSPENDED with readOnly true in remote_config—license-suspend-vs-revoke details operator UX.

Platform ops edit remote_config through admin HTML or ops API; product teams set customer-facing messaging for planned upgrades. configVersion mismatch triggers Hesabi to refresh local cache on next heartbeat—realtime policy without revoking JWT on every banner tweak.

Explicit non-goals

  • Postgres or distributed database migration
  • React admin for license service
  • Embedded license logic in CRM, Listmonk, or sync-worker
  • Customer self-service billing portal in v1
  • Replacing Hesabi cloud superadmin for routine key issuance
Why SQLite instead of Postgres?

Hundreds of instances with periodic heartbeats fit one WAL-mode file on a VPS volume. Backups are file snapshots. Scale target does not justify Postgres operational cost for this control plane.

Where do Hesabi plans and features live?

Opaque JSON on activation and instance rows. Hesabi app maps plan codes to modules. Server does not embed accounting-specific matrices.

What is the difference between suspend and revoke?

Suspend sets read-only with banner while keeping JWT and key; revoke clears activation and forces re-activation. See license-suspend-vs-revoke for operator UX.

Does DarDev Ops issue customer keys?

Ops deploys, DNS, and health. Product cloud superadmin issues keys via ops API. Platform admin is break-glass only.

Will 20.tn use the same plane?

Yes—P5 registers second solution_id with scoped ops key and same activate and heartbeat contract. Products share infrastructure, not databases.

Get company news

Releases and announcements — confirm from your inbox.

Subscribe to updates