- Go 69.8%
- Vue 24.8%
- Nix 2.7%
- Shell 1.4%
- JavaScript 0.8%
- Other 0.5%
| .pi | ||
| internal | ||
| script | ||
| ui | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| lefthook.yml | ||
| main.go | ||
| README.md | ||
| start-despacito.sh | ||
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
-
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).
-
Notify despacito. The action runner sends an HTTP
POSTrequest 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" } -
Update inputs. Despacito validates the request, then updates the relevant Nix flake input (e.g., bumping the
my-webappinput in the deployment flake'sflake.nixorflake.lock). -
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 tokenexpires_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
tokenvalue 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
localhostredirect 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 Conflictrather 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, andlog/slogas the foundation. External dependencies are added sparingly.
Development
Prerequisites
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