Docs / Console

Set up the console

The console is an optional web application for your teams: organizations, projects, branches, databases, a SQL editor, restore, monitoring, alerts and API keys for the CLI and agents. It runs in its own namespace, skela-cloud, as three workloads (the console, its API and a background worker) and keeps its state in a PostgreSQL database you operate, the management database. Sign-in uses your own WorkOS environment; SkelaDB never sees your users.

Install the platform first (install.md). The console then talks to the platform’s controller in each region you configure.

A read/write project API key can execute SQL and reveal credentials for every database in that project, including its main branch. A branch’s writes do not reach its parent, but a project key is not limited to that branch. Keep production in a separate project from agent automation, or give an agent only its disposable branch’s database credential.

1. WorkOS

Create a WorkOS environment for this installation.

  1. Under Redirects, add the callback https://console.example.com/api/auth/callback and the logout return URI https://console.example.com/.
  2. Create organization roles with the slugs admin and member (owner is also accepted as an administrator role). Organization administrators manage every project; members need a reader or developer grant per project, set in the console under Team & access.
  3. Under Webhooks, add https://api.example.com/webhooks/workos and subscribe to user.*, organization.*, organization_membership.* and session.*. Each event makes the API drop cached identity reads, so a removed member loses access on the next request. Events never grant access.
  4. Turn on “MFA required” before you name any platform operator (below); the API requires MFA for operator sessions.
  5. For sign-in and invitation mail, configure WorkOS’s custom email provider (Amazon SES or your own) so messages come from your domain.

Note the client ID, the API key, the webhook signing secret and the exact issuer of the environment.

2. Management database

Use a managed PostgreSQL (RDS, Cloud SQL, Azure Database for PostgreSQL) or a PostgreSQL you operate (we test with PostgreSQL 18), reachable from the skela-cloud namespace. Create a database and an owner role for the console, and turn on point-in-time recovery. The connection URL must use sslmode=verify-full; for a private CA, mount the CA file and add sslrootcert=<path> to the URL.

Plan connections: each API replica uses SKELA_CLOUD_DB_POOL_SIZE (default 16), and each worker that plus SKELA_CLOUD_WORKER_CONCURRENCY (default and minimum 2). One worker slot is reserved for URL imports; the remaining slots handle lifecycle operations even while an import is copying data.

3. Encryption keys

The management database stores database passwords, import sources, saved scripts, alert destinations and query history sealed with keys that only your installation holds. Generate one and escrow it before you start:

echo "1:$(openssl rand -base64 32)"

SKELA_CLOUD_ENCRYPTION_KEYS is a comma-separated list of id:base64key, newest first, where id is a number from 1 to 255. Losing every key that sealed a value loses that value; restoring a database backup without its keys leaves its credentials unreadable. See backup-and-restore.md and, to rotate, rotate-keys-and-certificates.md.

4. Regions and project slots

The console places each new project in a region: a platform installation and a prepared project identity (a slot). Prepare slots on the platform cluster:

  1. Create the project identity with the provider script from install.md, for example aws/project.sh slot-001.

  2. Give the slot its own API token in the skela-project-tokens Secret, which maps a project name to a token. A project token reaches only its own project:

    kubectl -n skela create secret generic skela-project-tokens \
      --from-literal=slot-001="$(openssl rand -hex 32)" --dry-run=client -o yaml | kubectl apply -f -

    To add slots later, add keys to the same Secret. The controller reads it on every request, so a replaced or removed token stops working at once. Slot tokens must be at least 32 characters.

  3. Describe the region in a JSON file, which you store in the cloud-regions Secret (mounted at /etc/skela):

    [{
      "id": "aws-us-east-1",
      "name": "US East (Virginia)",
      "provider": "aws",
      "postgres_majors": [18],
      "separated_postgres_majors": [18],
      "controller_url": "https://skela-control.skela.svc:8080",
      "controller_ca": "/etc/skela/control-ca.pem",
      "compute_ca": "/etc/skela/compute-ca.pem",
      "proxy_host": "sql.example.com",
      "proxy_port": 6432,
      "slots": [{"id": "slot-001", "token": "<the slot's token>"}]
    }]

    controller_ca and compute_ca are the CA certificates of your cert-manager issuer, stored as keys of the same Secret; each key becomes a file directly under /etc/skela:

    kubectl -n skela-cloud create secret generic cloud-regions \
      --from-file=regions.json --from-file=control-ca.pem=ca.crt --from-file=compute-ca.pem=ca.crt

    proxy_host and proxy_port are what your users connect to: the SKELA_TLS_DNS_NAME you installed with and the proxy’s load balancer port. Never put the controller’s admin token in a slot. Release 0.0.1 offers PostgreSQL 18 only. A region in object storage mode lists 18 in separated_postgres_majors, and new projects default to object storage there.

Each project uses one slot. Deleted projects keep their slot until their retained backups are purged after SKELA_CLOUD_DELETION_RETENTION_DAYS (default 7).

5. Settings

The API and worker read the cloud-config Secret; the console reads console-config. Create both in skela-cloud (the namespace is created by the console manifests; create it first with kubectl create namespace skela-cloud if you create the Secrets before installing).

cloud-config (API and worker):

Variable Meaning
SKELA_CLOUD_DATABASE_URL Management database URL with sslmode=verify-full.
SKELA_CLOUD_ENCRYPTION_KEYS Sealing keys, id:base64key with numeric IDs 1 to 255, newest first.
SKELA_CLOUD_REGIONS_FILE Path of the regions file, for example /etc/skela/regions.json.
SKELA_WORKOS_CLIENT_ID, SKELA_WORKOS_API_KEY From your WorkOS environment.
SKELA_WORKOS_ISSUER The exact issuer of the environment. Tokens from any other issuer or client are refused.
SKELA_WORKOS_WEBHOOK_SECRET Webhook signing secret.
SKELA_CLOUD_OPERATOR_IDS Comma-separated WorkOS user IDs (user_...) of platform operators. Anything else stops startup.
SKELA_CLOUD_ORG_CREATORS Who besides operators may create organizations: user IDs, email addresses or @domain entries, comma-separated. Unset: operators only. Everyone else joins by invitation.
SKELA_CLOUD_MAX_PROJECTS_PER_ORG Active projects per organization, default 10; 0 removes the limit.
SKELA_CLOUD_METRICS_TOKEN Bearer token, at least 32 characters, for the API’s /metrics.
SKELA_CLOUD_MAIL_FROM and mail settings Product mail (alerts, notifications); see below.
SKELA_CLOUD_AUDIT_RETENTION_DAYS Days the audit trail is kept, default 365 (365 to 3650).
SKELA_CLOUD_DELETION_RETENTION_DAYS Days a deleted project’s backups are kept before purge, default 7 (1 to 90).
SKELA_CLOUD_IMPORT_ALLOW_PRIVATE CIDRs of private networks that database URL imports may read from. Public addresses are always allowed.

console-config (console):

Variable Meaning
SKELA_CONSOLE_ORIGIN https://console.example.com.
SKELA_WORKOS_REDIRECT_URI https://console.example.com/api/auth/callback.
SKELA_WORKOS_CLIENT_ID, SKELA_WORKOS_API_KEY Same as the API.
SKELA_WORKOS_COOKIE_PASSWORD At least 32 random characters (openssl rand -base64 32). Changing it signs everyone out.
SKELA_CLOUD_API_URL http://cloud-api.skela-cloud.svc.cluster.local:8090 with SKELA_CLOUD_API_INTERNAL=1. Any other API URL must be HTTPS.
SKELA_PUBLIC_API_URL The HTTPS address your users and agents reach the API at, for example https://api.example.com. The console shows it in the CLI & agents snippets; unset, they show a placeholder.
SKELA_CONSOLE_TRUSTED_PROXIES Pod CIDRs of your ingress controller, so the audit trail records the real client address.
SKELA_CONSOLE_IDLE_MINUTES Sign out after this many idle minutes, default 60 (5 to 1440).
SKELA_CONSOLE_SESSION_HOURS Longest session, default 168 (1 to 720). Operator sessions always end after 30 minutes.

The console checks every setting when it starts and exits with a list of the problems instead of failing later. It refuses the example cookie password.

6. Mail

Product mail is sent by the worker.

  • Amazon SES: set SKELA_CLOUD_MAIL_FROM, SKELA_CLOUD_MAIL_CONFIGURATION_SET and AWS_REGION, and give the worker’s identity ses:SendEmail for that sender. For bounce and complaint handling, route the configuration set’s events through SNS to a dedicated SQS queue and set SKELA_CLOUD_FEEDBACK_QUEUE to its URL; bounced and complaining addresses are then suppressed.
  • SMTP (Google Cloud, Azure or any relay): set SKELA_CLOUD_SMTP_URL and SKELA_CLOUD_MAIL_FROM. smtps://user:password@smtp.example.com:465 uses implicit TLS, smtp://user:password@smtp.example.com:587 requires STARTTLS. Percent-encode reserved characters in the credentials. Handle bounces at the relay.

Allow the worker’s egress to the mail service.

7. Ingress and TLS

Expose the console and the API through your TLS-terminating ingress controller. The API needs only /v1 and /webhooks/workos on the public side. Set SKELA_INGRESS_NAMESPACE to your ingress controller’s namespace (default ingress-nginx); the console’s NetworkPolicies admit traffic only from there. To have the installer render the Ingress objects too, set SKELA_INGRESS_CLASS, SKELA_CONSOLE_HOST, SKELA_API_HOST and SKELA_INGRESS_TLS_SECRET (a TLS Secret in skela-cloud for both hosts, for example issued by cert-manager with ACME).

Database URL imports from providers on the public internet need SKELA_CLOUD_PUBLIC_IMPORTS=1, which allows PostgreSQL egress to public addresses. Alert destinations (Slack, webhooks) and WorkOS need HTTPS egress.

8. Install

Install the platform first (install.md). Then, with the same release loaded and your kubectl context set to the platform cluster (this step uses the current context), run the console release:

set -a; . ./release.env; set +a
kubectl config use-context "$SKELA_KUBE_CONTEXT"
cloud/render.sh apply

apply runs this release’s database migration Job and waits for it. If the migration fails it prints the Job’s log and stops, leaving any running release untouched. It then rolls out the API, worker and console and waits for each. The workloads only check the schema; they never migrate it, and they refuse to start against a schema older than their release. cloud/render.sh app prints the workload manifests for review without applying anything.

9. First sign-in and the first operator

  1. Open the console and sign in.
  2. Open Profile settings from the account menu and copy your User ID (user_...).
  3. Add it to SKELA_CLOUD_OPERATOR_IDS in cloud-config and restart the API: kubectl -n skela-cloud rollout restart deployment/cloud-api.
  4. The Platform entry appears in the account menu. Operators see platform health, regions and capacity.

Create your organization (operators and SKELA_CLOUD_ORG_CREATORS only), invite your team, then create a project: first-project.md.

Health and logs

The console serves /healthz and /readyz (ready only while the API answers); the worker serves both on port 8091. Every request gets an X-Request-Id that appears in both console and API logs. Logs are JSON lines and never contain tokens, passwords, connection strings or query text. Request bodies over 2 MiB are refused.