Skip to content

Walkthrough — Postgres + API server

This walkthrough deploys a real production stack: a Postgres database on Neon, an API server on Fly.io, with credentials auto-injected into the API process. End-to-end with rollback verified.

What you’ll have at the end: https://my-app-api.fly.dev/health returns 200, the database URL is a real Neon connection string, and ss inspect shows the deploy receipts.

  • Neon account — free tier works
  • Fly.io account + flyctl installed (the provisioner uses the Machines API but flyctl auth is the easiest way to get a token)
  • A starsystem.yaml file in your project root
starsystem.yaml
version: "2"
name: my-app
services:
app-db:
type: database
description: Cold-layer Postgres for app data.
capabilities: [relational]
depends_on: []
api:
type: process
description: HTTP API server.
depends_on: [app-db]
inject_from:
app-db:
- DATABASE_URL # auto-injected at deploy time
starsystem.prod.yaml
version: "2"
environment: prod
services:
app-db:
target:
type: neon
region: us-east-2
credential_env: NEON_API_KEY
project_name: my-app
api:
target:
type: fly
app: my-app-api
region: iad
image: ghcr.io/me/my-app-api:latest
credential_env: FLY_API_TOKEN
port: 3000
health_check_path: /health
env:
NODE_ENV: production

The inject_from block on the api service tells Starsystem: at deploy time, look up DATABASE_URL in the vault under the app-db namespace and pass it into the API process’s env. So Neon writes the credential after provisioning, and Fly reads it when launching the container — no manual copy-paste.

Terminal window
$ ss vault set neon.api_token nk_...
$ ss vault set fly.api_token fo1_...

Or use the device-flow auth helpers:

Terminal window
$ ss auth fly # opens browser; pastes back automatically
$ ss auth neon # paste-token flow

The vault is AGE-encrypted at ~/.starsystem/vault/. Never committed.

Terminal window
$ ss validate --env=prod
starsystem.yaml (2 services)
starsystem.prod.yaml (2 services)
Configuration is valid.
Terminal window
$ ss plan --env=prod
+ app-db neon · will create (Neon project + branch + role + db)
+ api fly · will create (Fly app + machine, region=iad)
Plan: 2 to create, 0 to update, 0 unchanged.
Terminal window
$ ss deploy --env=prod
app-db via neon...
Project created: my-app-quiet-mountain-1234
DATABASE_URL written to vault
api via fly...
App created: my-app-api
Machine launched in iad
Health check passing at https://my-app-api.fly.dev/health
Deploy complete: 2 provisioned

Provisioner order respects depends_on — Neon completes before Fly starts, so inject_from resolution works.

Terminal window
$ ss inspect --env=prod
ss inspect ed060a44-5f97-461c-8ff6-3d8cae953092
status: completed
duration: 87.3s
started: 2026-05-02 14:32:01 UTC
ended: 2026-05-02 14:33:28 UTC
── Timeline ──
ss.deploy.service.app-db (12.4s)
ss.deploy.service.api (74.9s)
── Next actions ──
Check drift ss drift --env=prod
Re-deploy ss deploy --env=prod
List deploys ss inspect --list --env=prod

Pass --state for the full provision state (URLs, resource IDs, metadata) or --json for machine-readable output.

Terminal window
$ curl https://my-app-api.fly.dev/health
{"status":"ok"}
$ ss inspect ed060a44 --env=prod --state | grep url
url: postgresql://...neon.tech/neondb...
url: https://my-app-api.fly.dev

Make a config change (bump the image tag), redeploy, then roll back:

Terminal window
$ vim starsystem.prod.yaml # change image to v2
$ ss deploy --env=prod # rolling update; image swapped
$ ss rollback --env=prod --list # see deploy history
$ ss rollback --env=prod --to=ed060a44
Rolled back to deploy ed060a44 credentials and overlay state restored.
Re-deploy services that depend on rolled-back credentials.

ss rollback does NOT auto-re-provision the infrastructure (the v2 image is still running on Fly). It restores the vault credentials and the overlay YAML’s _state block to the historical snapshot. Run ss deploy again to re-converge to the rolled-back config.

Set up a nightly check:

.github/workflows/drift.yml
on: { schedule: [{ cron: "0 6 * * *" }] }
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm install
- run: pnpm --filter @celestial/starsystem-cli build
- run: ./packages/starsystem-cli/dist/cli.js drift --env=prod --json
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

If something drifts, the workflow exits non-zero and you get a notification.

Terminal window
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io/v1/traces
export OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=$HONEYCOMB_KEY
ss deploy --env=prod

Now every deploy emits spans:

  • ss.cli.deploy (root)
  • ss.deploy (engine)
  • ss.provision.neon (per service)
  • ss.provision.fly

Open Honeycomb, filter by service.name=starsystem, see your deploys flowing as traces. Click any deploy span → the next-actions in the inspect view link back into the trace.

Terminal window
ss validate --env=prod # schema check
ss plan --env=prod # preview
ss deploy --env=prod # apply
ss inspect [<id>] [--env=prod] # receipts view
ss inspect --list [--env=prod] # deploy history
ss drift --env=prod # detect out-of-band changes
ss rollback --env=prod --to=<id> # restore from snapshot
ss destroy --env=prod # tear down (with confirmation)
ss ssh / exec / logs <service> # day-2 ops over SSH (VPS providers)
ss graph render --services # mermaid topology diagram
  • Provisioner errors → check vault credentials with ss vault get neon.api_token
  • Fly health-check failsss logs api --env=prod (uses fly logs under the hood)
  • Plan shows “skip” → no provisioner registered for the target type; check for typos
  • Drift detected, no obvious change → may be vendor-side; check the provider dashboard