spivot-server

module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 21, 2026 License: Apache-2.0

README

Spivot Server

CI Release Go Reference Go Report Card OCI Image

Spivot Server is the reference Go back end for OpenCaravan, an open protocol for coordinating group drives over networks. The server is being built alongside the first OpenCaravan specification so the protocol has a practical, operable reference implementation from the beginning.

The primary release artifact is a well-labeled, OCI-compliant multi-arch container image. The current release line includes the complete client-app enrollment + session macaroon auth stack, the first protected CRUD endpoints (journey + telemetry), and a documented HTTP/3

  • mTLS Traefik recipe an operator can stand up in under fifteen minutes.

Project Status

Spivot Server is pre-release software. Expect API and schema changes while the OpenCaravan protocol vocabulary settles.

Current foundation:

  • Go HTTP service with a small main shim around a testable run entry point.
  • Structured logging with log/slog.
  • Health, readiness, version, and server discovery endpoints.
  • In-band server policy snapshot advertisement.
  • Server-local certificate authority (spivot-server ca init) that issues short-lived client app certificates.
  • Single-use invite tokens for client app enrollment, with first-run bootstrap logging for unattended container deployments.
  • POST /v1/client-apps/enroll HTTP endpoint that redeems a server_registration invite + CSR for a 7-day signed leaf certificate.
  • Identity middleware that resolves a presented client certificate (direct mTLS or proxy-forwarded) to its enrolled (user_id, client_app_id) via the issued_certificates audit table.
  • Macaroon issuer + verifier (internal/platform/auth/macaroon) backed by a macaroon_roots HMAC keystore. The OpenCaravan caveat vocabulary (time<T, journey=, user=, client_app=, action=) is enforced at verify time, with unknown predicates rejected fail-closed.
  • POST /v1/sessions endpoint that issues a session macaroon for an authenticated client app. One macaroon authorizes one SessionAction against an optional journey; multi-action sessions are deferred to a future protocol extension.
  • Session validation middleware (AttachSession + RequireSession) that lifts the Authorization: Macaroon ... header, verifies the macaroon signature once per request, and provides composable per-handler constraints (action match, journey path parameter match, custom). Mirrors the two-tier shape of the identity middleware.
  • First end-to-end protected endpoints: POST /v1/journeys (identity-only), GET /v1/journeys/{id} (journey-scoped session macaroon), and POST /v1/journeys/{id}/telemetry (journey-scoped + telemetry.write session macaroon, plus a participant-membership check). Validates the full auth stack composes correctly before unlocking the rest of the protocol API surface.
  • Container-first release engineering with OCI labels and health checks.
  • Embedded SQL migration metadata for OpenCaravan journey storage.

Requirements

  • Go 1.26+
  • just
  • Docker, for container builds and the full CI gate
  • golangci-lint v2, for local linting

Quick Start

git clone https://github.com/wheelsdown/spivot-server.git
cd spivot-server

just ci
just serve

In another shell:

curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/v1/version

You can also run the command directly while developing:

go run ./cmd/spivot-server serve

Common Workflows

All project workflows go through the justfile.

just build          # Build for the current platform -> dist/
just build-linux    # Build linux/amd64 and linux/arm64 binaries
just container      # Build a local OCI image
just container-run  # Run the local dev image on port 8080
just test           # Run tests with the race detector
just lint           # Run golangci-lint v2
just ci             # Full local gate

just ci is the required local gate before pushing changes. It runs formatting checks, module tidiness, linting, race tests, container build validation, OCI label checks, spivot-server version, and a live container health check.

API Surface

The current HTTP surface is deliberately small:

GET  /                          service summary
GET  /health                    liveness check
GET  /readyz                    readiness check
GET  /v1/server                 server discovery, capabilities, and policy snapshot
GET  /v1/version                build and runtime version metadata
POST /v1/client-apps/enroll     redeem a server_registration invite + CSR
                                for a signed leaf certificate
POST /v1/sessions               (requires client cert) issue a session
                                macaroon for a single SessionAction,
                                optionally scoped to a journey
POST /v1/journeys               (requires client cert) create a new
                                journey with the caller as host
GET  /v1/journeys/{id}          (requires session macaroon scoped to
                                the journey + journey.read) load journey
POST /v1/journeys/{id}/telemetry
                                (requires session macaroon scoped to
                                the journey + telemetry.write) record
                                a telemetry batch

Future OpenCaravan API routes will be documented as they land. Go package documentation is part of the public reader experience; exported symbols and packages should have useful Godoc comments.

OpenCaravan Protocol Version

The /v1/server.protocol.version field advertises the OpenCaravan wire-format version this server implements (currently 0.1.0). The protocol version is decoupled from the spivot-server release version: spivot-server releases that fix bugs or add capabilities without changing the wire format do not bump the protocol version, and spivot-server releases that consume a new wire format from opencaravan-go bump the protocol version in lockstep with that module's tag.

The 0.x prefix signals pre-stable; a 1.0 protocol release will mark the wire format as frozen. A client should treat 0.1.x and 0.1.y as compatible (additive extensions only), and a leading-digit change (e.g., 0.1 → 0.2) as a breaking wire-format revision that needs matching client work.

In practice, when reading a deployed server: spivot-server v0.X.Y and OpenCaravan vA.B.C are independent vectors. A long divergence (server at v0.5.0 while protocol is still 0.1.0) is informative, not a bug — it tells a client author that the wire format has been stable for many server releases.

Certificate Authority

Spivot Server acts as its own certificate authority for the client apps that enroll with it. The CA's keypair and self-signed root certificate are generated on demand and persisted under <data-dir>/identity/:

spivot-server ca init        # generate keypair + self-signed root if absent
spivot-server ca cert        # print the CA's certificate as PEM

ca init is idempotent: re-running it loads the existing CA and prints its fingerprint. The key is written with 0600 permissions and is never logged. Subject defaults to CN=Spivot Server CA; override with --common-name and --organization flags (or SPIVOT_CA_COMMON_NAME / SPIVOT_CA_ORGANIZATION env vars).

Every leaf certificate the CA signs is recorded in the issued_certificates audit table (serial, subject, validity window, issuance time, revocation time). The identity middleware resolves a presented client certificate's serial back to its enrolled (user_id, client_app_id) through that table, so revoking a row by setting revoked_at is sufficient to break the identity binding — short-lived (7-day) leaf certs make CRL/OCSP infrastructure unnecessary for v0.

Client App Enrollment Invites

Spivot Server uses single-use invite tokens to gate which apps may enroll. Each token carries a scope (server_registration for new users, journey for joining a private journey), an expiration, and a one-time-use guarantee. Only the SHA-256 hash of the token is stored on disk; the plaintext is shown to the operator exactly once at issuance.

First-run bootstrap

The first time a fresh server starts with zero registered users and no active server_registration invite, it self-issues a 24-hour invite and prints a fenced banner to its stdout. The expected operator flow:

docker run ... ghcr.io/wheelsdown/spivot-server:latest serve
...
████████████████████████████████████████████████████████████████████
  SPIVOT SERVER FIRST-RUN BOOTSTRAP
  ────────────────────────────────────────────────────────────────
  No administrator is registered. Use this server_registration
  invite to enroll the first user. Single-use, 24h expiry.

      <43-character base64url token>

  iOS app: Settings → Add Account → Use Invite
████████████████████████████████████████████████████████████████████

The operator copies the token from container logs into the first administrator's app. Subsequent restarts while the bootstrap invite is still active stay silent. Once a user is registered, the bootstrap path never runs again.

Day-two invites

After bootstrap, additional invites are issued by an administrator via the CLI (later phases will add an authenticated HTTP endpoint):

spivot-server invite create                         # 24h server_registration invite
spivot-server invite create -scope journey -lifetime 168h    # 7 days

The output includes the plaintext token, the scope, the expiration time, and the stored token hash for audit correlation.

Storage Schema

The first schema foundation lives in internal/platform/storage. Migrations are embedded with Go's standard embed package so the server can expose and apply the exact schema it was built with.

The current schema covers, by capability area:

  • Identity and accounts — accounts, account devices, vehicles.
  • Journeys — journeys with per-journey policy snapshots, journey invites, journey participants and consent, participant sessions, and journey segments.
  • Telemetry — telemetry batches and position samples.
  • Auth — issued_certificates (audit trail of every leaf cert the CA signs: serial, subject, validity window, issuance time, revocation time), client_app_invites (hashed invite tokens with scope and one-time-use semantics), client_apps (one row per enrolled app installation), and macaroon_roots (HMAC root keys the session macaroon issuer signs against — rotated rows retained so macaroons signed under a since-rotated key remain verifiable until their own time<T caveat fires).
  • Federation and policy — server policy snapshots, federated servers (placeholder, federation isn't wired yet).

The schema stores protocol-facing data conservatively: text identifiers, RFC3339 timestamp strings, integer-scaled coordinates, hashed invite tokens, and JSON extension documents. Runtime storage uses SQLite, with the embedded migrations applied at startup before the server begins handling API traffic.

Container deployments reserve /etc/spivot for operator configuration and /var/lib/spivot for durable state. The default SQLite database path is /var/lib/spivot/spivot.db in the container and data/spivot.db for local development unless SPIVOT_DATABASE_PATH is set.

Containers

just container performs a multi-arch build (defaults to linux/amd64,linux/arm64) via docker buildx. Output is a per-arch docker save-compatible tarball under dist/ plus the host-arch variant loaded into the local Docker daemon for just container-run:

just container
# → dist/spivot-server-linux-amd64.tar
# → dist/spivot-server-linux-arm64.tar
# → ghcr.io/wheelsdown/spivot-server:dev loaded into the local daemon

To smoke-test a release candidate against a remote deploy host, push the same multi-arch build to GHCR:

echo "$(gh auth token)" | docker login ghcr.io -u "$(gh api user -q .login)" --password-stdin
just container-push ghcr.io/wheelsdown/spivot-server:dev

Tagged releases are intended to be consumed from GitHub Container Registry:

docker pull ghcr.io/wheelsdown/spivot-server:latest

For production Docker Compose deployments behind an existing reverse proxy, see docs/deployment/docker-compose.md. For the canonical HTTP/3 + mTLS deployment recipe (Traefik in front, client cert termination, worked enrollment walkthrough), see docs/deployment/reverse-proxy.md and examples/deploy/traefik/mtls/.

Release Engineering

Release tags use semver with a leading v, such as v0.1.0 or v0.1.0-rc.1.

just prepare-release v0.1.0
just publish-release v0.1.0

The convenience wrapper runs both phases:

just release-github v0.1.0

The release workflow publishes multi-architecture OCI images to GHCR on release tags. Version data is injected at build time; release versions should not be hardcoded in Go source.

Repository Layout

cmd/spivot-server/       command entry point and CLI parsing
internal/app/            process lifecycle wiring
internal/server/api/     HTTP API handlers
internal/server/middleware/  identity + session attach/require middleware
internal/platform/       build info, identity (CA + key store), auth
                         (macaroon issuer/verifier), logging, proxy
                         (forwarded-headers handling), storage, and
                         shared platform code
scripts/releng/          release engineering helpers
docs/deployment/         operator-facing deployment recipes
examples/deploy/         reverse-proxy example configurations

Contributing

See AGENTS.md for the project conventions used by Codex and other automation:

  • prefer the Go standard library
  • keep PRs focused
  • use conventional commits
  • run just ci locally before every push
  • keep exported Go symbols and packages documented for Godoc

License

Spivot Server is licensed under the Apache License, Version 2.0. See LICENSE and NOTICE for the full text and attribution.

"Spivot" is a trademark of the Spivot project. The code license does not grant trademark rights; see TRADEMARK.md for what you can do with the name.

Directories

Path Synopsis
cmd
spivot-server command
Spivot Server is the backend API service for the Spivot iOS app.
Spivot Server is the backend API service for the Spivot iOS app.
internal
app
Package app wires the process lifecycle around the HTTP API server.
Package app wires the process lifecycle around the HTTP API server.
platform/auth/macaroon
Package macaroon issues and verifies the session-bound macaroons spivot-server hands out in exchange for a successful POST /v1/sessions request.
Package macaroon issues and verifies the session-bound macaroons spivot-server hands out in exchange for a successful POST /v1/sessions request.
platform/buildinfo
Package buildinfo holds version and build metadata stamped at compile time via ldflags.
Package buildinfo holds version and build metadata stamped at compile time via ldflags.
platform/identity
Package identity manages Spivot Server's cryptographic identities: the server-local certificate authority that signs short-lived client app certificates, and the private-key storage backing it.
Package identity manages Spivot Server's cryptographic identities: the server-local certificate authority that signs short-lived client app certificates, and the private-key storage backing it.
platform/logging
Package logging contains shared logging helpers.
Package logging contains shared logging helpers.
platform/proxy
Package proxy contains helpers for interpreting reverse-proxy request metadata without making the HTTP server depend on a specific edge proxy.
Package proxy contains helpers for interpreting reverse-proxy request metadata without making the HTTP server depend on a specific edge proxy.
platform/storage
Package storage owns database schema metadata for Spivot Server.
Package storage owns database schema metadata for Spivot Server.
server/api
Package api implements the Spivot HTTP API.
Package api implements the Spivot HTTP API.
server/middleware
Package middleware wires the HTTP authentication boundary for spivot-server: it lifts whatever client identity the lower layers presented (direct mTLS or trusted-proxy-forwarded cert headers, extracted by github.com/wheelsdown/spivot-server/internal/platform/proxy.RequestInfoFrom), resolves it to a server-side Identity via the issued-certificates audit table, attaches that Identity to the request context, and provides a per-handler guard that 401s when an Identity is required but absent.
Package middleware wires the HTTP authentication boundary for spivot-server: it lifts whatever client identity the lower layers presented (direct mTLS or trusted-proxy-forwarded cert headers, extracted by github.com/wheelsdown/spivot-server/internal/platform/proxy.RequestInfoFrom), resolves it to a server-side Identity via the issued-certificates audit table, attaches that Identity to the request context, and provides a per-handler guard that 401s when an Identity is required but absent.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL