Id RFC 007 admin service and cli
RFC-007 — Admin Service & CLI
- *tatus:*Draft (revised 2026
0518 to reconcile with shipped CLI in #073) - *ate:*2026
0408 - *uthor:*Koder Team
- *epends on:*RFC
001, RFC002
Summary
The Admin service provides tenant management, system configuration, OAuth2 client registration, and API key management. The CLI (koder-id-cli) is a command-line client that communicates with the Admin service via gRPC.
Admin Service Responsibilities
- *enant management*— create, configure, suspend tenants (organizations)
- *Auth2 client registration*— register apps that use Koder ID for authentication
- *PI key management*— create/revoke machine
tomachine keys - *ystem configuration*— global and per-tenant settings
- *udit log access*— query audit trail
- *ootstrap*— initial system setup (first tenant, first admin user)
API Endpoints (REST) — actual shipped surface
Reality as of 20260518 (see services/admin/internal/handler/http.go::RegisterRoutes). Tables below are normative; plannedbutnot-shipped endpoints are called out in a separate *uture*sub-section.
Tenants
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/admin/tenants |
List tenants |
| POST | /v1/admin/tenants |
Create tenant (name, slug, domain) |
| GET | /v1/admin/tenants/{id_or_slug} |
Get tenant |
| PUT | /v1/admin/tenants/{id_or_slug} |
Update tenant |
| DELETE | /v1/admin/tenants/{id_or_slug} |
Suspend tenant |
API Keys
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/admin/api-keys |
List API keys |
| POST | /v1/admin/api-keys |
Create API key |
| DELETE | /v1/admin/api-keys/{id} |
Revoke API key |
Audit, System, Bootstrap
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/admin/audit |
Query audit log |
| GET | /v1/admin/system/health |
Health check |
| GET | /v1/admin/system/metrics |
Prometheus metrics |
| POST | /v1/admin/bootstrap |
Idempotent bootstrap of required tenants |
OAuth2 Clients — served by the OAuth service, not Admin
Client management is *ot*under /v1/admin/.... The OAuth service exposes both gRPC (OAuthService.{CreateClient, GetClient,
UpdateClient, DeleteClient, ListClients}) and a narrow HTTP surface:
| Method | Endpoint | Service | Description |
|---|---|---|---|
| POST | /oauth/v2/register |
oauth | RFC 7591 Dynamic Client Registration — gated by KODER_ID_DCR_BOOTSTRAP_TOKEN |
| POST | /v1/admin/clients/{client_id}/reset-secret |
oauth | Rotate client secret — gated by KODER_ID_MIGRATION_MODE |
| POST | /v1/admin/migrate/oauth-client |
oauth | Migration helper for legacy → v2 clients |
The CLI (koder-id-cli client {register,list,get,delete,rotate-secret}) talks to OAuth gRPC for CRUD and to /v1/admin/clients/{id}/reset-secret for rotation.
Future REST surface (not yet shipped)
| Method | Endpoint | Description | Status |
|---|---|---|---|
| POST | /v1/admin/oauth/clients |
Mirror CreateClient under admin namespace | not impl |
| GET | /v1/admin/oauth/clients |
Mirror ListClients under admin namespace | not impl |
| DELETE | /v1/admin/oauth/clients/{id} |
Mirror DeleteClient under admin namespace | not impl |
| GET/POST | /v1/admin/tenants/{id}/users/... |
Admin-side user management | not impl (Identity service has its own surface) |
| GET/PATCH | /v1/admin/system/config |
Runtime config CRUD | not impl |
gRPC Service Definitions
The CLI consumes two gRPC services. Both live under pkg/proto/koder/id/{admin,oauth}/v1/.
koder.id.admin.v1.AdminService (port 4005)
Actual shipped RPCs (see pkg/proto/koder/id/admin/v1/admin_grpc.pb.go):
service AdminService {
rpc CreateTenant(CreateTenantRequest) returns (CreateTenantResponse);
rpc GetTenant(GetTenantRequest) returns (GetTenantResponse);
rpc UpdateTenant(UpdateTenantRequest) returns (UpdateTenantResponse);
rpc DeleteTenant(DeleteTenantRequest) returns (DeleteTenantResponse);
rpc ListTenants(ListTenantsRequest) returns (ListTenantsResponse);
rpc CreateAPIKey(CreateAPIKeyRequest) returns (CreateAPIKeyResponse);
rpc ListAPIKeys(ListAPIKeysRequest) returns (ListAPIKeysResponse);
rpc RevokeAPIKey(RevokeAPIKeyRequest) returns (RevokeAPIKeyResponse);
rpc QueryAuditLog(QueryAuditLogRequest) returns (QueryAuditLogResponse);
rpc Bootstrap(BootstrapRequest) returns (BootstrapResponse);
rpc Health(HealthRequest) returns (HealthResponse);
rpc Metrics(MetricsRequest) returns (MetricsResponse);
}Note: RegisterClient, DisableUser, EnableUser, ForceLogout, GetConfig, UpdateConfig were planned in earlier RFC revisions but were *ot*implemented. The first three live in OAuth and Identity services respectively (see below); the config RPCs were dropped in favor of env-var configuration.
koder.id.oauth.v1.OAuthService (port 4003)
CRUD for OAuth clients (consumed by the CLI's client subcommands):
service OAuthService {
rpc CreateClient(CreateClientRequest) returns (CreateClientResponse);
rpc GetClient(GetClientRequest) returns (GetClientResponse);
rpc UpdateClient(UpdateClientRequest) returns (UpdateClientResponse);
rpc DeleteClient(DeleteClientRequest) returns (DeleteClientResponse);
rpc ListClients(ListClientsRequest) returns (ListClientsResponse);
// ... plus token/authorize/introspect RPCs out of CLI scope
}Bootstrap Flow
The bootstrap endpoint (POST /v1/admin/bootstrap, AdminService.Bootstrap) is *dempotent* it ensures a named set of tenants exists and returns status per tenant. Safe to re-run.
$ koder-id-cli bootstrap
Bootstrap complete:
✓ Tenant "koder" (slug: koder)
✓ Tenant "crescer" (slug: crescer) [skipped — already exists]
# Verify mode prints status without making changes
$ koder-id-cli bootstrap --check --tenants koder,crescer,vivver
TENANT STATUS ID
koder ok 01KR87BDSNMEQT55R39J1HFERZ
crescer ok 01KS12...
vivver MISSING -The legacy --tenant-name/--tenant-slug/--admin-email/--admin-password flags from earlier drafts were *ever implemented*and are not supported. Admin user creation happens outofband today (drop-in SQL/kdbnext seed); productionizing the adminuser path is tracked separately.
OAuth Client Bootstrap (without CLI subcommand pre-#073)
For historical context: before #073 added client register, the only way to create the first OAuth client (Flow, Kall, etc.) was via the RFC 7591 DCR endpoint, gated by a single-use bootstrap token. This flow remains the supported way to create the very first client (when no CLI is yet deployed to the host):
- Drop a systemd override on
koder-id-oauth.servicewithKODER_ID_DCR_BOOTSTRAP_TOKEN=<one-time-secret>. POST /oauth/v2/registerwithAuthorization: Bearer <secret>and the RFC 7591 client metadata body.
- Capture
client_id+client_secretfrom the response. - Remove the drop-in and restart the service.
After step 4, all subsequent client management goes through koder-id-cli client register (which talks to OAuth gRPC directly and does not depend on the DCR token).
CLI — koder-id-cli
Shipped binary: /usr/local/bin/koder-id-v2-cli, with symlink /usr/local/bin/koder-id-cli. Source: engine/cli/cmd/.
Configuration
The CLI is stateless and configured exclusively via environment variables. There is no config file.
| Env var | Default | Purpose |
|---|---|---|
KODER_ID_ADMIN_ADDR |
localhost:4005 |
Admin service gRPC address |
KODER_ID_OAUTH_ADDR |
localhost:4003 |
OAuth service gRPC address |
KODER_ID_OAUTH_HTTP_ADDR |
localhost:4013 |
OAuth service HTTP address (used by client rotate-secret) |
KODER_ID_FORMAT |
table |
Output format — table or json |
Commands
Flat verb-noun dispatch (no cobra, no global flag namespace). Help mirrors the shipped binary:
Usage: koder-id-cli <command> [flags]
Commands:
bootstrap Initial system setup
bootstrap --check [--tenants koder,crescer] Verify required tenants exist
tenant create|list|get <slug> Manage tenants
client list|get|register|delete|rotate-secret Manage OAuth clients
api-key create|list|revoke Manage API keys
keys init|add|get|list Koder Keys vault operations
health Check system health
test-oidc [--issuer URL] [--tenant SLUG] Validate OIDC discovery + JWKS
smoke [--tenant SLUG] [--product NAME] [--issuer URL]
Liveness smoke test
metrics [--window DURATION] Print service health as JSON
version Show version
help Show this helpclient subcommands (added in #073):
koder-id-cli client list --tenant <slug>
koder-id-cli client get --tenant <slug> --client-id <id>
koder-id-cli client register --tenant <slug> --name <n> [--type confidential] \
[--redirect <url>]... [--grant <gt>]... [--scope <s>]...
koder-id-cli client delete --tenant <slug> --client-id <id>
koder-id-cli client rotate-secret --tenant <slug> --client-id <id>Tenant identifier resolution: any subcommand that takes --tenant
<slug> accepts either a slug ("koder") or a 26-char ULID; the CLI auto-resolves slug → ULID via admin GetTenant.
Tenant slug vs storage tenant
The string passed via --tenant is the *ublic slug*(e.g. koder), which is what RFC-006 §Q1 and the tenant claim in id_tokens (#069) reference. This is distinct from DEFAULT_TENANT / KDBNEXT_TENANT env vars that the engine binaries read — those name the *torage tenant*in kdb-next where the tables live, and may differ (e.g. prod runs with DEFAULT_TENANT=koder-id while the user-facing tenant slug is koder).
Example Usage
# Bootstrap — idempotent; safe to re-run
$ koder-id-cli bootstrap
# Verify required tenants are provisioned
$ koder-id-cli bootstrap --check
TENANT STATUS ID
koder ok 01KR87BDSNMEQT55R39J1HFERZ
crescer MISSING -
vivver MISSING -
# Create tenants
$ koder-id-cli tenant create --name Crescer --slug crescer --domain crescer.koder.dev
$ koder-id-cli tenant create --name Vivver --slug vivver --domain vivver.koder.dev
# Register an OAuth client (Flow as OIDC RP)
$ koder-id-cli client register \
--tenant koder \
--name "koder-flow" \
--type confidential \
--redirect "https://flow.koder.dev/user/oauth2/koder-id/callback" \
--grant authorization_code --grant refresh_token \
--scope openid --scope profile --scope email
# List clients (table; KODER_ID_FORMAT=json for JSON)
$ koder-id-cli client list --tenant koder
# Rotate a client secret (requires KODER_ID_MIGRATION_MODE=true on the server)
$ koder-id-cli client rotate-secret --tenant koder --client-id 01KR77G98C2WRKT4
# Spot-check OIDC discovery + JWKS for a tenant
$ koder-id-cli test-oidc --issuer https://id.koder.dev --tenant koder
# Liveness smoke test
$ koder-id-cli smoke --tenant koder --issuer https://id.koder.dev
# Health check across all sub-services
$ koder-id-cli healthAuthorization Model
The Admin service uses a simple role-based model:
| Role | Scope |
|---|---|
system_admin |
Full access to all tenants and system config |
tenant_admin |
Full access within their tenant only |
tenant_viewer |
Read-only access within their tenant |
Roles are stored as claims in the user's access token:
{
"roles": ["system_admin"],
"tenant_id": "koder"
}Interaction with Other Services
┌────────┐
│ Admin │─── gRPC ──▶ Identity Service (user disable/enable)
│Service │─── gRPC ──▶ OAuth Service (client registration)
│ │─── gRPC ──▶ Session Service (force logout)
└────────┘
▲
│ gRPC
┌────────┐
│ CLI │
└────────┘Security Considerations
- Admin API today is unauthenticated on the loopback interface (see
services/admin/cmd/main.go); the LXC firewall keeps it off the public network. Adding bearer/mTLS auth is a separate, tracked ticket (backlog/pendingadmin-auth gate; #098 documents the gap). - The OAuth
/v1/admin/clients/{id}/reset-secretHTTP endpoint and/v1/admin/migrate/oauth-clientare gated byKODER_ID_MIGRATION_MODE=trueenv var (a temporary opt-in until proper admin auth lands). - DCR (
POST /oauth/v2/register) is gated byKODER_ID_DCR_BOOTSTRAP_TOKENper #070/#072 fix. - Bootstrap is idempotent (not one
shot), so rerunning it is safe. - Client secrets are shown only once on creation; stored as base64
SHA-256.
- API keys are shown only once on creation; stored as base64 SHA-256.
- All admin operations are audit-logged via
services/audit.
Changelog
- 2026
0518 — Reconcile with shipped CLI (#073): swap plannedvsrealREST tables, replace the legacy bootstrap flags, add OAuth bootstrap flow, replace the multi-RPC AdminService draft with the actual shipped RPCs, document
--tenantslugvsstorage distinction surfaced in #073, fix the security section to reflect that admin auth is still tracked. - 2026
0408 — Initial draft.