Skip to content

BuildService Port & Frontend Strategy

§16.3 BuildService Port Interface

BuildService port · internal/port/build.go
type BuildService interface {
    EnsureProject(ctx context.Context, workspaceID string) (ProjectRef, error)
    SubmitBuild(ctx context.Context, spec BuildSpec) (*BuildHandle, error)
    StreamLogs(ctx context.Context, handle BuildHandle) (<-chan LogLine, error)
    GetBuildStatus(ctx context.Context, handle BuildHandle) (*BuildStatus, error)
    CancelBuild(ctx context.Context, handle BuildHandle) error
}

EnsureProject is idempotent. It creates a Depot project on first call for a workspace; subsequent calls return the existing project reference. This enforces per-customer cache isolation (a Depot requirement for building untrusted code).

§16.4 Per-Customer Isolation

Depot requires one project per customer entity to prevent cache poisoning between tenants. Starform maps this 1:1 to workspaces (not users, not projects — workspaces):

  • On workspace creation, Starbase calls EnsureProject and stores the Depot project ID
  • All builds for services in that workspace run inside that Depot project
  • Cache is scoped per workspace (one customer's cached layers cannot be read by another)
  • Cache quota per Depot project: 30 GB default, configurable per tier later

§16.5 Frontend Strategy

Starform uses Railpack as the primary build frontend, with Dockerfile as the explicit fallback. This matches the git-push-and-deploy UX that Railway, Render, Heroku, and Vercel all provide. Requiring a Dockerfile at the entry point would put Starform in a visibly weaker position on the pricing page and the "getting started" flow — the two surfaces where prospects compare platforms.

Resolution order at build time:

  1. If the repo root contains a Dockerfile, use it. Customer intent is explicit; respect it.
  2. Otherwise, run Railpack against the repo. Railpack inspects the source (looks for package.json, requirements.txt, go.mod, Gemfile, etc.) and emits a BuildKit LLB dependency graph.
  3. If Railpack cannot detect a supported framework, fail the build with a clear error message pointing the customer toward either adding a Dockerfile or adjusting their project structure.

Why Railpack specifically:

  • Open source (MIT), Go-based, maintained by Railway
  • Successor to Nixpacks with significantly smaller image sizes (38% smaller Node images, 77% smaller Python images per Railway's public benchmarks)
  • Emits BuildKit LLB natively — integrates cleanly with Depot's lower-level BuildKitService API without requiring a synthetic Dockerfile intermediate step
  • Same build system Railway uses today, which means the ecosystem of framework detection logic is maintained by the market leader in developer PaaS
  • Library-mode integration: Starbase Worker imports Railpack as a Go dependency, calls it in-process. No separate binary, no subprocess orchestration.

Depot integration paths:

  • Dockerfile path uses Depot's high-level BuildService.CreateBuild endpoint with the Dockerfile frontend. Simple, well-supported, matches depot build CLI behavior.
  • Railpack path uses Depot's BuildKitService endpoint. Railpack generates LLB; Starbase Worker submits LLB directly to Depot's BuildKit endpoint. This bypasses Depot's Dockerfile frontend entirely and runs the build using whatever LLB Railpack produced.

Both paths use the same per-workspace Depot project, so cache isolation and billing work identically.

Vendor risk note

Railpack is maintained by Railway, a direct Starform competitor. The MIT license makes forking possible if Railway ever gates features or changes direction. Starform treats Railpack as a vendored dependency — pinned to a specific version, upgraded deliberately, with a fork always available as insurance.


Cross-references

The MVP flow that exercises both Depot paths → §16.2 · the BuildService port in the broader port set → Starbase §10 · per-workspace isolation maps to the billing boundary workspace_id§24.1 · build-minute billing → §16.7 · known limitations of this design → §16.8.