Skip to content

Services & Environments

The core service model: services are project-scoped definitions, instantiated per environment. A service exists in an environment iff a service_environments row exists — per-env presence, version, branch, size, and placement are fully independent, while identity (service_id) stays stable across environments, as §20.2/§38.2/FR-077 assume. This page owns the model's tables and endpoints. (Design record: specs/2026-07-11-database-schema-design.md.)

api worker
staging ✓ 1 replica · develop branch
production ✓ 3 replicas · main branch (not instantiated here)

Tables

Service model · definitions + per-environment instantiation · Starbase Postgres
services (
    id                  UUID PRIMARY KEY,
    project_id          UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    name                TEXT NOT NULL,          -- slug; drives hostname generation (§39.1 #8)
    type                TEXT NOT NULL CHECK (type IN ('web', 'worker', 'cron')),  -- cron post-MVP (§39.3 #48)
    source_type         TEXT NOT NULL DEFAULT 'git' CHECK (source_type IN ('git', 'image')),  -- git build vs prebuilt image (templates, §39.3)
    image_ref           TEXT,                   -- pinned image when source_type='image' (e.g. ghost:5); NULL for git
    git_installation_id UUID REFERENCES github_installations(id),
    git_repo            TEXT,                   -- "owner/repo" (git source)
    build_config        JSONB NOT NULL DEFAULT '{}',   -- root_directory · dockerfile_path · build/start/pre_deploy commands (§16.5)
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    deleted_at          TIMESTAMPTZ,            -- soft delete: tombstone protocol (§25.1)
    UNIQUE (project_id, name)
);

service_environments (          -- FR-077's "service-per-environment record"
    id                    UUID PRIMARY KEY,
    service_id            UUID NOT NULL REFERENCES services(id) ON DELETE CASCADE,
    environment_id        UUID NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    cluster_id            UUID REFERENCES clusters(id),      -- placement (FR-077)
    branch                TEXT,                 -- branch→env mapping (§39.1 #17); NULL = manual deploys
    auto_deploy           BOOLEAN NOT NULL DEFAULT TRUE,     -- #17 opt-out flag
    current_deployment_id UUID,                 -- FK added with deployments (Build Lifecycle)
    tier                  TEXT NOT NULL DEFAULT 'mininova',  -- Nova ladder; resources derived at render (§24.2)
    replicas              INT NOT NULL DEFAULT 1,
    autoscaling           JSONB,                -- {enabled,min,max,cpu,mem} → §25.1 block
    public                BOOLEAN NOT NULL DEFAULT TRUE,     -- web only: false = internal-only, no HTTPRoute/DNS (§39.1 #10)
    hostname              TEXT UNIQUE,          -- public URL <name>-<rand4>.starform.app (public web only); customer-editable (§39.1 #8)
    internal_host         TEXT,                 -- ClusterIP Service name <service-name>-<environment>; shown + auto-injected (§39.1 #10)
    suspended             BOOLEAN NOT NULL DEFAULT FALSE,    -- billing suspension (§36.3)
    health_check          JSONB,                -- {path?, port, timeout_seconds} → §20.3 probe mapping
    created_at            TIMESTAMPTZ DEFAULT NOW(),
    deleted_at            TIMESTAMPTZ,
    deletion_confirmed_at TIMESTAMPTZ,          -- Shuttle tombstone ack (§25.4)
    UNIQUE (service_id, environment_id)
);

service_connections (           -- service-to-service wiring (§39.1 #10); same environment only
    id                          UUID PRIMARY KEY,
    from_service_environment_id UUID NOT NULL REFERENCES service_environments(id) ON DELETE CASCADE,
    to_service_environment_id   UUID NOT NULL REFERENCES service_environments(id) ON DELETE CASCADE,
    env_var_key                 TEXT NOT NULL,  -- injected into the from-service (default <TARGET_NAME>_URL)
    created_at                  TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (from_service_environment_id, env_var_key)  -- value = http://<to.internal_host>:<port>
);

Changing type or source_type

Both are set at creation. Changing a service's type (web/worker/cron) from the dashboard is a delete-and-rebuild of the type-specific resources — web→worker drops the Service/HTTPRoute/DNS record, worker→web adds them — which Shuttle already does by reconciling the full resource set; service_id is preserved, so metrics and log history survive. public, hostname, and scaling are ordinary live edits.

API endpoints

Conventions (auth, errors, pagination, Developer* notation) → Dashboard API.

Method Path Permission Purpose
GET / POST /projects/{id}/services Viewer / Developer List · create definition (name conflict → 409)
GET / PATCH / DELETE /services/{id} Viewer / Developer / Proj Admin Read · edit repo/build_config · delete (tombstone)
GET /services/{id}/environments Viewer The env matrix (where it runs, with what config)
PUT /services/{id}/environments/{env_id} Developer* Create/update the instantiation (payload below)
DELETE /service-environments/{id} Developer* Remove from this env (tombstone)
POST /service-environments/{id}/restart Developer* Zero-downtime rolling restart (§38.7 checksum)
GET / PUT / DELETE /service-environments/{id}/connections Viewer / Developer* List · set · remove service-to-service links (auto-injected, §39.1 #10)
Instantiate / configure · PUT /services/{id}/environments/{env_id}
{
  "cluster_id": null,              // null = Starbase places it (project default region, FR-077)
  "branch": "develop",             // branch→env mapping (#17); null = manual deploys only
  "auto_deploy": true,
  "public": true,                  // web only: false = internal-only, no public URL (§39.1 #10)
  "tier": "nova",
  "replicas": 2,
  "autoscaling": { "enabled": true, "min_replicas": 2, "max_replicas": 6, "target_cpu_percent": 75 },
  "health_check": { "path": "/healthz", "port": 3000, "timeout_seconds": 300 }
}

Deploying into an instantiation (builds, deployments, rollback) → Build Lifecycle.

Networking & connections

Three exposure levels, from type + public:

type · public Serves HTTP ClusterIP Service Public URL + DNS Reachable by
web · public=true ✓ (<name>-<rand4>.starform.app, §39.1 #8) the internet
web · public=false same-project, same-env services
worker / cron nobody (calls outbound only)
  • Internal address (shown to the customer). A web service-env's ClusterIP Service is named <service-name>-<environment> (stored as internal_host); siblings reach it at http://<service-name>-<environment>:<port> — e.g. http://api-production:8080. The dashboard shows this short host:port, never the proj-<uuid>.svc.cluster.local FQDN (that stays under an "advanced" reveal). It resolves because same-project services share one namespace, and the §20.4 NetworkPolicy already scopes reachability to same project, same environment. Cross-region has no private path — use the public hostname (FR-078). Workers/cron have no internal address.
  • Connections (auto-inject). Linking a consumer → a target in the same environment (service_connections) makes Starbase inject env_var_key (default <TARGET_NAME>_URL) = http://<target.internal_host>:<port> into the consumer's env, resolved per environment — the same path that injects DB credentials, so service and database wiring behave alike. The raw internal address stays available for anything a link doesn't cover; cross-service ${{…}} reference variables remain deferred (a templating engine).

Cross-references

The full data-model map → Data Model · the environments table → RBAC §15.7 · what Shuttle renders per instantiation → §20.2 · the payload entry it becomes → §25.1 · hostname → region routing → DNS & Domains · probes & pre-deploy → §20.3 · suspension → Billing §36.3. Canonical map: Canonical Sources.