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:
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
- 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.
- Account
name uniqueness — unique among active accounts only (current proposal) or globally? Active-only is more flexible for reuse of deactivated names.
- 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.
- Future: API token sub-resource —
POST /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)
Status:
proposedComponent:
router·api·schema·configDepends 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 withBearerAuthmiddleware from the auth middleware feature. The existingrouter.Newsignature gains anAdminReader/AdminWriterinterface (or reuses a combined store interface if the existing one covers it).Endpoints
Configuration
GET /configreturns 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 /configaccepts 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
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):
DELETEis a soft deactivate — setsdeactivated_at, does not remove the row. This preserves referential integrity for future foreign keys from API tokens or other resources.POSTrequest body:{ "name": "my-client" }POSTresponse (201 Created):{ "id": "...", "name": "my-client", "created_at": "..." }GET /accountsresponse:{ "items": [ { "id": "...", "name": "my-client", "created_at": "...", "active": true } ] }DELETEresponse: 204 No Content on success, 404 if not found, 409 if already deactivated.Store Interface
A new
AdminWriterinterface (or extension of the existing writer if one exists):The existing
db.Readeranddb.Writer(ordb.Store) may absorb these — TBD based on whether admin operations belong alongside ingest operations in the same interface.Schema Changes
New migration adding the
accountstable (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
httptestand a fake/mock storeintegration_testpackage pattern):GET /api/v1/admin/configwithout token → 401GET /api/v1/admin/configwith token → 200 with redacted keyOpen Questions
nameuniqueness — unique among active accounts only (current proposal) or globally? Active-only is more flexible for reuse of deactivated names.db.Storeor a separateadmin.Store? Separate keeps ingest-path interfaces narrow; combined is simpler for tests.POST /api/v1/admin/accounts/:id/tokensis the natural next endpoint. The account table and soft-delete pattern are designed to support this without migration changes.Non-Goals