Skip to content

Admin API #60

Description

@446564

Status: proposed
Component: router · api · schema · config
Depends on: Authentication Middleware feature
Relates to: future API token auth · future account management


Summary

Add an authenticated admin API surface under /api/v1/admin/. The initial scope covers two areas: runtime configuration inspection/mutation, and account management (create, list, deactivate accounts). All admin endpoints require the bearer token introduced by the auth middleware feature. This API is the extensibility point for future operator-facing capabilities including per-client API token issuance.


Background

Beacon currently has no operator interface beyond editing config files and restarting the process. As the feature set grows (multi-account support, per-client API tokens, ingest configuration), an in-process admin API allows runtime changes without downtime and sets up a foundation for a future admin UI.

The Admin API is deliberately narrow in v1 — no UI, no pagination complexity on config endpoints, no bulk operations. The goal is to establish the patterns (auth boundary, route prefix, handler structure, schema migrations) that future admin features will follow.


Design

Route Prefix

All admin routes are mounted under /api/v1/admin/ and wrapped with BearerAuth middleware from the auth middleware feature. The existing router.New signature gains an AdminReader/AdminWriter interface (or reuses a combined store interface if the existing one covers it).

Endpoints

Configuration

GET  /api/v1/admin/config          → Returns current effective config as JSON
PUT  /api/v1/admin/config          → Updates mutable config values at runtime

GET /config returns the sanitized running config — API key value is redacted, all other fields included. This is useful for verifying what the server is actually running with.

PUT /config accepts a partial JSON body of mutable fields. Initially mutable fields are limited to CORS origins and ingest worker count. Immutable fields (DB DSN, listen address) are ignored or rejected with 400. Changes take effect immediately where possible; fields requiring restart are flagged in the response.

Response shape for GET:

{
  "cors": {
    "allowed_origins": ["*"]
  },
  "ingest": {
    "worker_count": 4
  },
  "auth": {
    "api_key": "[redacted]"
  }
}

Accounts

GET    /api/v1/admin/accounts           → List all accounts
POST   /api/v1/admin/accounts           → Create a new account
GET    /api/v1/admin/accounts/:id       → Get account by ID
DELETE /api/v1/admin/accounts/:id       → Deactivate account (soft delete)

Accounts represent operator-defined entities that can own resources (API tokens, allowed IATAs, etc.) in future features. In v1, an account is simply a named record with an active flag.

Account schema (new table):

CREATE TABLE accounts (
    id          UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
    name        TEXT        NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    deactivated_at TIMESTAMPTZ
);

CREATE UNIQUE INDEX accounts_name_uidx ON accounts (name) WHERE deactivated_at IS NULL;

DELETE is a soft deactivate — sets deactivated_at, does not remove the row. This preserves referential integrity for future foreign keys from API tokens or other resources.

POST request body:

{
  "name": "my-client"
}

POST response (201 Created):

{
  "id": "...",
  "name": "my-client",
  "created_at": "..."
}

GET /accounts response:

{
  "items": [
    {
      "id": "...",
      "name": "my-client",
      "created_at": "...",
      "active": true
    }
  ]
}

DELETE response: 204 No Content on success, 404 if not found, 409 if already deactivated.

Store Interface

A new AdminWriter interface (or extension of the existing writer if one exists):

type AdminWriter interface {
    CreateAccount(ctx context.Context, name string) (*Account, error)
    DeactivateAccount(ctx context.Context, id uuid.UUID) error
}

type AdminReader interface {
    ListAccounts(ctx context.Context) ([]*Account, error)
    GetAccount(ctx context.Context, id uuid.UUID) (*Account, error)
}

The existing db.Reader and db.Writer (or db.Store) may absorb these — TBD based on whether admin operations belong alongside ingest operations in the same interface.


Schema Changes

New migration adding the accounts table (see above). No changes to existing tables.


Config Changes

No new config fields beyond what the auth middleware feature adds. The admin API is always present when the server starts — it is protected by auth, not conditionally compiled or enabled.


Testing

  • Unit tests per handler (config GET/PUT, accounts CRUD) using httptest and a fake/mock store
  • Integration tests (following existing integration_test package pattern):
    • GET /api/v1/admin/config without token → 401
    • GET /api/v1/admin/config with token → 200 with redacted key
    • Full account lifecycle: create → list → get → deactivate → list confirms inactive
    • Duplicate account name → 409 or 400 (constraint violation handled gracefully)

Open Questions

  1. Config mutability scope — which fields are safe to mutate at runtime vs require restart? Needs a defined list before implementation to avoid partial-restart footguns.
  2. Account name uniqueness — unique among active accounts only (current proposal) or globally? Active-only is more flexible for reuse of deactivated names.
  3. Store interface placement — absorb admin methods into db.Store or a separate admin.Store? Separate keeps ingest-path interfaces narrow; combined is simpler for tests.
  4. Future: API token sub-resourcePOST /api/v1/admin/accounts/:id/tokens is the natural next endpoint. The account table and soft-delete pattern are designed to support this without migration changes.

Non-Goals

  • Admin UI (separate project or future feature)
  • Role-based access control within the admin API (single admin key means all-or-nothing)
  • Account login / session management (accounts are operator-defined records, not auth principals at this stage)
  • Pagination on account list (acceptable at small operator scale; add when needed)

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requestp3-lowLow priority / nice to have

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions