Running a deployment
Install the stack, activate it with your key, and keep it current. A fully hosted tenant arrives with all of this done, so this page is for operators running Veblen on their own server.1. Install
One command clones the deploy shell to /opt/veblen, asks for your domain, admin
account, and registry token, writes .env, and starts the stack.
bash <(curl -fsSL https://git.sotekzone.com/judah/veblen-public/-/raw/main/contrib/install.sh)
To do it by hand, clone the same repository, copy .env.example to .env and fill it, then authenticate to the admin registry with the deploy token this portal issued
for your deployment.
docker login registry.sotekzone.com -u <token-name> -p <token> docker compose --profile production pull docker compose --profile production up -d
The production profile runs the app image behind nginx, alongside Postgres and Letta. VEBLEN_VERSION pins the image tag, and an empty value tracks latest.
Set COMPOSE_PROFILES=production in .env to drop the flag from every
command.
2. Activate
A deployment serves nothing until it holds a valid license grant. Until then every route
redirects to /setup. There are three ways past it.
| Path | How | When to use it |
|---|---|---|
| Setup wizard | Open /setup and paste the license key | The normal first boot |
| Headless | Set LICENSE_KEY and VEBLEN_DEPLOYMENT_ID in .env | Scripted or rebuilt deployments |
| Offline grant | Paste a signed grant issued by the admin | An install that cannot reach this portal |
Both values appear in your dashboard when the deployment is created. Activation stores the signed grant, and the asset classes named in it are the ones the deployment can enable.
3. Credentials
| Credential | Purpose | Issued by |
|---|---|---|
LICENSE_KEY | Entitlement, tier, and asset classes | This portal, per deployment |
VEBLEN_DEPLOYMENT_ID | Binds the key to this deployment | This portal, shown with the key |
| Registry deploy token | docker pull the Veblen image | This portal, backed by a GitLab deploy token |
RECONCILE_TOKEN | Local cron to the deployment's own reconciler | You, with openssl rand -hex 32 |
Everything else in .env is yours, the Stripe and PayPal keys that collect your
money, the SMTP mailbox that sends your mail, and the market-data keys you hold accounts for.
None of it reaches this portal.
4. How the license stays current
A running deployment re-verifies once a day and caches the signed answer it gets back. It keeps honoring a cached grant for 14 days past expiry, so a portal outage, a lapsed renewal, or a weekend with no network never interrupts an appraisal in progress.
When verification finally fails, the deployment locks to its /setup screen. Nothing
is deleted and the database is untouched, so activating a valid key brings the practice back
exactly as it was.
5. Verifying a license
POST https://veblen.dev/api/license/verify
Content-Type: application/json
{
"license_key": "vbl_...",
"deployment_id": "...",
"version": "v20.4.1"
} The response carries the decision and a signed JWT.
{
"valid": true,
"reason": null,
"tier": "revenue_share",
"mau_cap": 0,
"asset_classes": ["vehicle", "personal_property"],
"expires_at": null,
"token": "eyJhbGciOiJFUzI1NiIs...",
"cache_seconds": 86400
} Verify the token against /.well-known/jwks.json and cache it. A zero mau_cap means uncapped, which is what both pricing models issue,
since users are never metered. asset_classes lists the classes the deployment may enable,
and the same list is signed into the token, so editing the database cannot widen it.
6. Heartbeat
POST https://veblen.dev/api/heartbeat
Content-Type: application/json
{
"license_key": "vbl_...",
"version": "v20.4.1",
"schema_tip": "0042_add_testimonials",
"mau_count": 18,
"ledger_balanced": true
} Sent hourly by the deployment. The response echoes the license status and whether the reported user count is over cap. Your dashboard marks a deployment stale after three silent days, which is the signal that a server went down rather than a license going bad.
7. Denial reasons
| Reason | Meaning |
|---|---|
license_revoked | Key revoked, or not recognized |
license_suspended | Suspended in the portal |
license_expired | Past its expiry date |
subscription_canceled | The account's subscription ended |
subscription_unpaid | Dunning exhausted without payment |
deployment_mismatch | The key belongs to a different deployment |
A past_due subscription still verifies. Suspension is an explicit step once dunning
is exhausted, so one failed card charge does not take a practice offline.
8. Upgrades and backups
Upgrading is a pull and a recreate. The app lands any new schema modules itself on first boot, so there is no migration step to run.
docker compose --profile production pull docker compose --profile production up -d --remove-orphans
Back up Postgres and Letta together with the script in the deploy shell. It reads the database
name and user out of your .env and keeps seven days.
contrib/backup/pg_backup_all.sh /opt/veblen /opt/backups/pgdb
Run it from host cron rather than inside the app, so it keeps working on the days the stack is down, which are the days a backup matters most.
0 2 * * * /opt/veblen/contrib/backup/pg_backup_all.sh /opt/veblen >> /var/log/pg_backup.log 2>&1