An API for receiving build information and triggering Colmena changes
  • Go 69.8%
  • Vue 24.8%
  • Nix 2.7%
  • Shell 1.4%
  • JavaScript 0.8%
  • Other 0.5%
Find a file
2026-06-09 21:07:23 +00:00
.pi Add initial take no pi agent project file 2026-06-09 01:26:07 +00:00
internal Add revocation field for auth tokens and DELET /api/tokens/<id> 2026-06-09 20:59:52 +00:00
script Migrate to zerolog 2026-06-09 18:47:32 +00:00
ui Add the UI action to revoke a token. 2026-06-09 21:07:23 +00:00
.gitignore Add beginning of frontend and sessions 2026-06-08 23:17:51 +00:00
flake.lock Set up nix build 2026-05-31 23:25:50 +00:00
flake.nix Migrate to zerolog 2026-06-09 18:47:32 +00:00
go.mod Migrate to zerolog 2026-06-09 18:47:32 +00:00
go.sum Migrate to zerolog 2026-06-09 18:47:32 +00:00
lefthook.yml Migrate to zerolog 2026-06-09 18:47:32 +00:00
main.go Move to port 9000 to avoid conflict with MITMProxy 2026-06-09 18:50:38 +00:00
README.md Add session to API 2026-06-09 02:01:13 +00:00
start-despacito.sh Add start script for running things 2026-06-09 01:26:22 +00:00

despacito

A bridge between Forgejo Actions and Colmena — the steady, deliberate heartbeat of your continuous delivery pipeline.

Overview

despacito is a lightweight Go service that sits between your Forgejo CI runners and Colmena deployment tool. It receives build-completion notifications from Forgejo Actions, updates Nix flake inputs, and triggers Colmena deployments — slowly, carefully, and reliably.

┌─────────────────┐     HTTP POST      ┌──────────────┐    shell out    ┌──────────┐
│  Forgejo Action │ ──────────────────► │  despacito   │ ──────────────► │ Colmena  │
│     Runner      │   (JSON payload)    │              │                 │  Deploy  │
└─────────────────┘                     └──────┬───────┘                 └──────────┘
                                              │
                                              │ read/write
                                              ▼
                                       ┌──────────────┐
                                       │  Nix Flake   │
                                       │  (flake.nix) │
                                       └──────────────┘

How It Works

  1. Build & Push. A Forgejo Action builds a Nix package and pushes the closure to Attic (a Nix binary cache). On success, the runner obtains a Nix package identifier (e.g., a store path or flake reference).

  2. Notify despacito. The action runner sends an HTTP POST request to the despacito endpoint with a JSON body describing what was built:

    {
      "package": "my-webapp",
      "version": "1.4.2",
      "nix_store_path": "/nix/store/abc123...-my-webapp-1.4.2",
      "flake_ref": "github:myorg/my-webapp/v1.4.2"
    }
    
  3. Update inputs. Despacito validates the request, then updates the relevant Nix flake input (e.g., bumping the my-webapp input in the deployment flake's flake.nix or flake.lock).

  4. Deploy. Despacito shells out to colmena apply, which evaluates the updated flake and deploys the new package to the target server(s).

Project Architecture

despacito/
├── main.go                       # Entry point: config, DB, OIDC, server startup
│
├── ui/                           # Vue 3 frontend (Vite)
│   ├── src/
│   │   ├── main.js               # Vue app bootstrap
│   │   └── App.vue               # Root component
│   ├── index.html
│   ├── package.json
│   └── vite.config.js
│
├── internal/
│   ├── config/
│   │   └── config.go             # Environment variable parsing
│   │
│   ├── database/
│   │   ├── database.go           # PostgreSQL connection pool
│   │   ├── migrate.go            # Goose migration runner
│   │   ├── tokens.go             # Auth token CRUD (Bearer tokens)
│   │   ├── sessions.go           # Session & user management (OIDC)
│   │   ├── errors.go             # Sentinel errors
│   │   └── migrations/           # SQL migration files
│   │
│   ├── oidc/
│   │   └── oidc.go               # OIDC provider discovery & setup
│   │
│   ├── server/
│   │   ├── server.go             # HTTP mux, routing, SPA handler
│   │   ├── middleware.go         # Auth (Bearer + session), logging, recovery
│   │   ├── handler.go            # Webhook + token management endpoints
│   │   └── auth.go               # OIDC login / callback / logout
│   │
│   ├── static/
│   │   ├── embed.go              # Embeds compiled Vue frontend
│   │   └── dist/                 # Vite build output (populated at build time)
│   │
│   ├── webhook/
│   │   ├── types.go              # Request/response types
│   │   └── validate.go           # Payload validation
│   │
│   └── version/
│       └── version.go            # Build-time git commit injection
│
├── go.mod
├── go.sum
├── flake.nix                     # Nix build for Go backend + Vue frontend
└── README.md

Authentication

despacito uses a two-tier authentication model:

1. Bearer Tokens (API / webhook access)

API tokens are stored in the database (auth_token table). Generate them via the POST /api/tokens endpoint.

Bootstrap: When no tokens exist in the database, POST /api/tokens is open to anyone (no auth required) so you can create your first token. Once any token exists, the endpoint requires authentication (Bearer token or OIDC session).

2. OIDC Sessions (browser / UI access)

Users sign in through an OpenID Connect provider. On successful login, a session is created and stored in a secure HTTP-only cookie. The session can be used to authenticate to the UI and to the token management API.

API

GET /api/oidc/providers

Public. Returns the list of configured OIDC providers so the frontend can render login buttons.

{
  "providers": [
    {"issuer": "https://accounts.example.com", "name": "accounts.example.com"}
  ]
}

GET /auth/login?issuer=<url>

Initiates the OIDC flow. Redirects the browser to the chosen provider's authorization URL. Accepts ?issuer= to select a specific provider.

GET /auth/callback

Handles the OIDC provider's redirect. Validates the CSRF state cookie, exchanges the authorization code, verifies the ID token, upserts the user, creates a session, and redirects to /.

POST /auth/logout

Revokes the current session and clears the session cookie.

POST /api/tokens

Creates a new Bearer token. Requires authentication (Bearer token or OIDC session), unless no tokens exist yet (bootstrap mode).

Request:

{
  "description": "ci-runner-1",
  "expires_in": "720h"
}
  • description — human-readable label for the token
  • expires_in — optional Go duration string (e.g. "720h", "30d"). If omitted, the token never expires.

Response (201):

{
  "id": "uuid",
  "token": "64-char-hex-string",
  "description": "ci-runner-1",
  "created_at": "2026-06-08T12:00:00Z",
  "expires_at": "2026-07-08T12:00:00Z"
}

Important: The token value is only returned once — store it securely.

POST /webhook

Called by Forgejo action runners when a build completes successfully.

Headers:

Header Value
Content-Type application/json
Authorization Bearer <token>

Request Body:

{
  "package": "string (required) — package name matching a flake input",
  "version": "string (required) — semantic version of the new build",
  "nix_store_path": "string (required) — /nix/store path of the built package",
  "flake_ref": "string (optional) — flake reference"
}

Responses:

Status Meaning
200 Request accepted, deployment triggered
400 Invalid or missing fields in request body
401 Missing or invalid authorization token
409 This version is already deployed (idempotent)
422 Package name does not match a known flake input
500 Internal error (flake update or deployment failed)

Success Response Body:

{
  "status": "deployed",
  "package": "my-webapp",
  "version": "1.4.2",
  "deployment_id": "d_abc123"
}

Configuration

despacito is configured entirely via environment variables:

Variable Required Default Description
DESPACITO_LISTEN No :8080 TCP address to listen on (ignored if DESPACITO_LISTEN_SOCKET is set)
DESPACITO_LISTEN_SOCKET No Unix domain socket path (takes precedence over DESPACITO_LISTEN)
DESPACITO_BASE_URL No Public base URL (e.g. https://despacito.example.com). Used for building redirect URIs and OIDC callback URLs.
DESPACITO_FLAKE_DIR No Path to the deployment flake directory (optional for now)
DESPACITO_COLMENA_BIN No colmena Path to the colmena binary
DESPACITO_LOG_LEVEL No info Log level (debug, info, warn, error)
DESPACITO_POSTGRES_CONNECTION_URI No PostgreSQL connection string. Required for authentication and persistence.
DESPACITO_SESSION_COOKIE_SECURE No true Set the Secure flag on session cookies. Set to false for local development without TLS.

OIDC Configuration

When OIDC is enabled, users can sign in through the web UI. You must register an OAuth2/OIDC application with your identity provider and configure these variables:

Variable Required Description
DESPACITO_OIDC_CLIENT_ID Yes (for OIDC) OAuth2 client ID from your provider
DESPACITO_OIDC_CLIENT_SECRET Yes (for OIDC) OAuth2 client secret from your provider
DESPACITO_OIDC_REDIRECT_URL Yes (for OIDC) Callback URL, e.g. https://despacito.example.com/auth/callback
DESPACITO_OIDC_ISSUER_URLS Yes (for OIDC) Comma-separated list of OIDC issuer URLs. Multiple providers are supported — the frontend will show a login button for each.

Example: Forgejo as an OIDC provider

Create an OAuth2 application in your Forgejo instance at Settings → Applications → Manage OAuth2 Applications. Set the redirect URI to https://despacito.example.com/auth/callback.

Then configure despacito:

export DESPACITO_POSTGRES_CONNECTION_URI="postgres://despacito:secret@localhost:5432/despacito?sslmode=disable"
export DESPACITO_OIDC_ISSUER_URLS="https://git.example.com"
export DESPACITO_OIDC_CLIENT_ID="your-client-id"
export DESPACITO_OIDC_CLIENT_SECRET="your-client-secret"
export DESPACITO_OIDC_REDIRECT_URL="https://despacito.example.com/auth/callback"
export DESPACITO_BASE_URL="https://despacito.example.com"
export DESPACITO_SESSION_COOKIE_SECURE="true"

Example: Multiple providers

export DESPACITO_OIDC_ISSUER_URLS="https://git.example.com, https://accounts.google.com"

Each provider's hostname is shown on the login button in the UI.

Example: Local development (no TLS)

export DESPACITO_SESSION_COOKIE_SECURE="false"
export DESPACITO_OIDC_REDIRECT_URL="http://localhost:8080/auth/callback"
export DESPACITO_BASE_URL="http://localhost:8080"

Note: Most OIDC providers require HTTPS for production. Some (like Forgejo) allow localhost redirect URIs for development.

Bootstrapping the first API token

On first run with an empty database, create your initial Bearer token:

curl -X POST http://localhost:8080/api/tokens \
  -H "Content-Type: application/json" \
  -d '{"description": "initial-admin-token"}'

This endpoint is open when no tokens exist. After creating your first token, all subsequent calls to POST /api/tokens require authentication.

Alternatively, sign in through the web UI via OIDC, then create tokens from an authenticated session.

Design Decisions

  • One deployment at a time (by default). Colmena deployments are serialized to avoid conflicting flake updates and deployment races.
  • Idempotent. If a version is already deployed, despacito returns 409 Conflict rather than re-deploying unnecessarily.
  • Database-backed tokens. Auth tokens are stored in PostgreSQL, not a single shared secret. Tokens can be created, revoked, and expired individually.
  • OIDC for humans. Browser-based access uses OpenID Connect with session cookies. API/webhook access uses Bearer tokens.
  • Synchronous by default. The HTTP handler blocks until the deployment completes, giving the Forgejo runner immediate feedback.
  • Go standard library first. Uses net/http, encoding/json, and log/slog as the foundation. External dependencies are added sparingly.

Development

Prerequisites

  • Go 1.22+
  • Node.js 22+
  • Nix (optional, provides a fully reproducible dev shell)

Quick start with Nix

nix develop
# Drops you into a shell with go, node, and npm.

Backend (Go)

go build ./...
./despacito

Frontend (Vue 3)

cd ui
npm install
npm run dev        # Vite dev server with hot reload, proxies API to :8080

Open http://localhost:5173 in your browser. The Vite dev server proxies /api, /auth, /webhook, and /health requests to the Go backend on port 8080.

Building the frontend for embedding

cd ui
npm run build      # outputs to ui/dist/

The production build compiles the frontend into the Go binary. When running the binary, the embedded SPA is served automatically.

Full Nix build

nix build

This builds both the Vue frontend and the Go backend, embedding the compiled frontend into the resulting despacito binary.

Running the complete stack

# 1. Start a PostgreSQL database (e.g. via Docker or NixOS)
# 2. Set the required environment variables
export DESPACITO_POSTGRES_CONNECTION_URI="postgres://despacito:secret@localhost:5432/despacito?sslmode=disable"
export DESPACITO_OIDC_ISSUER_URLS="https://git.example.com"
export DESPACITO_OIDC_CLIENT_ID="your-client-id"
export DESPACITO_OIDC_CLIENT_SECRET="your-client-secret"
export DESPACITO_OIDC_REDIRECT_URL="https://despacito.example.com/auth/callback"

# 3. Run the server
./despacito

# Or with a Unix domain socket (for use behind Caddy or another reverse proxy)
export DESPACITO_LISTEN_SOCKET=/run/despacito/webhook.sock
./despacito

Reverse proxy with Caddy

despacito.example.com {
    reverse_proxy unix//run/despacito/webhook.sock
}

Note: When using a Unix socket, the socket file is automatically removed on shutdown. Stale socket files from a previous crashed run are cleaned up on startup.

Database setup

despacito uses goose for schema migrations. Migrations run automatically on startup if a database connection URI is configured. Tables created:

Table Purpose
users OIDC-authenticated users (issuer + subject unique)
sessions OIDC login sessions (linked to users)
auth_token Bearer tokens for API/webhook access

Testing the webhook

curl -X POST http://localhost:8080/webhook \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token-from-api/tokens>" \
  -d '{
    "package": "my-webapp",
    "version": "1.4.2",
    "nix_store_path": "/nix/store/abc123-my-webapp-1.4.2"
  }'

License

TBD