Documentation
¶
Overview ¶
Package adminapi exposes a read-only admin HTTP surface over the fabriq query facade. It is designed for consumption by the fabriq-admin SPA and any operator tooling that needs a stable, auth-agnostic read API over the data fabric.
The extension is auth-agnostic: the host MUST attach authentication and tenant-injection middleware via WithRouteOptions. Fabriq adminapi adds no auth of its own so that the host controls the security boundary entirely.
Phase 1 endpoints:
GET {BasePath}/meta → service metadata + capabilities
GET {BasePath}/entities → paginated entity list (requires ?type=)
GET {BasePath}/entities/{id} → single entity by type and id (requires ?type=)
GET {BasePath}/capabilities → fabric subsystem capabilities (instance-level,
or per-type with ?type=)
Index ¶
- Constants
- Variables
- func PluginRemoteSpec() registry.EntitySpec
- type Authorizer
- type AuthorizerFunc
- type Extension
- func (e *Extension) Dependencies() []string
- func (e *Extension) Description() string
- func (e *Extension) Name() string
- func (e *Extension) Ready(_ context.Context) error
- func (e *Extension) Register(app forge.App) error
- func (e *Extension) Start(ctx context.Context) error
- func (e *Extension) Stop(_ context.Context) error
- func (e *Extension) Version() string
- type IssuedKey
- type KeyRecord
- type KeySpec
- type KeyStore
- type KeyToucher
- type Option
- func WithAdminLogin(username, password string) Option
- func WithAnalyticsAdmin() Option
- func WithAnalyticsRead() Option
- func WithAuth(store KeyStore) Option
- func WithAuthDisabled() Option
- func WithAuthRequired() Option
- func WithAuthorizer(a Authorizer) Option
- func WithBasePath(p string) Option
- func WithConnectionsRead() Option
- func WithEmbedder(e agent.Embedder) Option
- func WithOperatorOnlyInstanceOps() Option
- func WithRouteOptions(opts ...forge.RouteOption) Option
- func WithSchemaAdmin() Option
- func WithTenantRegistry(r TenantRegistry) Option
- func WithTenantsAdmin() Option
- func WithWritePolicy(p agent.WritePolicy) Option
- func WithoutDefaultLogin() Option
- type TenantRecord
- type TenantRegistry
Constants ¶
const APIVersion = 1
APIVersion is the current major version of the adminapi wire contract. Clients may advertise the version they were built against via the X-Fabriq-Api-Version request header; the auth middleware rejects requests whose advertised major differs from this value (HTTP 426). Responses always carry the current APIVersion in the same header.
const PluginRemoteEntityType = "admin_plugin_remote"
PluginRemoteEntityType is the well-known dynamic entity type name for remote plugin specs. The host application must register a matching EntitySpec (with a DynamicSchema) before using these endpoints; see PluginRemoteSpec.
const Version = forgeext.Version
Version is the adminapi extension version.
Variables ¶
var ErrKeyNotFound = errors.New("adminapi: api key not found")
ErrKeyNotFound is returned by KeyStore.Revoke when no key has the given id.
Functions ¶
func PluginRemoteSpec ¶
func PluginRemoteSpec() registry.EntitySpec
PluginRemoteSpec returns the canonical EntitySpec for admin_plugin_remote. The host application calls reg.Register(adminapi.PluginRemoteSpec()) during startup so the entity is available to the command plane and the relational querier. In unit tests, register it via buildPluginWorld or equivalent.
Types ¶
type Authorizer ¶ added in v0.0.6
type Authorizer interface {
Authorize(ctx context.Context, capability string) (allowed bool, err error)
}
Authorizer decides whether the current request may exercise a named capability. ctx is the request context — it carries the resolved tenant and API-key id, plus any principal a host authz middleware (e.g. warden) stamped before the handler ran. A resource-free (ctx, capability) shape is the minimal common denominator: the tenant is already in ctx, so a per-tenant authz system reads it from there.
type AuthorizerFunc ¶ added in v0.0.6
AuthorizerFunc adapts a plain function to Authorizer.
type Extension ¶
type Extension struct {
forge.BaseExtension
// contains filtered or unexported fields
}
Extension exposes the fabriq data fabric as a read-only admin HTTP surface. It depends on the "fabriq" extension and resolves its query facade at Start.
func NewAdminAPI ¶
NewAdminAPI builds the adminapi extension wired to a started fabriq Extension. The endpoint is auth-agnostic: the host MUST attach authentication and tenant-injection middleware via WithRouteOptions.
func (*Extension) Dependencies ¶
Dependencies implements forge.Extension.
func (*Extension) Description ¶
Description implements forge.Extension.
func (*Extension) Ready ¶ added in v1.9.0
Ready reports whether Start has completed. The admin API is ready to serve once it has resolved the fabriq facade and bootstrapped its admin key.
func (*Extension) Register ¶
Register registers the admin controller routes; the fabric resolves lazily in Start.
type IssuedKey ¶
IssuedKey is returned once, at issue time. Key is the plaintext bearer token and is never persisted or recoverable afterwards — only its hash is stored.
type KeyRecord ¶
type KeyRecord struct {
ID string
Prefix string
TenantID string
Label string
CanManageKeys bool
CreatedAt time.Time
RevokedAt *time.Time
// ExpiresAt is nil for keys that never expire. Session tokens (issued via
// IssueSession) always set it; the authn middleware denies a resolved key
// once ExpiresAt is in the past, mirroring how RevokedAt is enforced.
ExpiresAt *time.Time
// LastUsedAt is when the key last authenticated a request, accurate to
// about a minute (the middleware throttles the write). Nil for a key that
// has not been used since last_used_at was added. Only List fills it:
// Lookup leaves it nil so the auth path never depends on the column.
LastUsedAt *time.Time
}
KeyRecord is a stored key as seen on the read paths. It deliberately carries NO hash or plaintext field: redaction is structural, so List can never leak a verifiable secret. TenantID == "" means the key is multi-tenant (NULL in the database).
type KeySpec ¶
KeySpec describes a key to be issued. TenantID == "" issues a multi-tenant key (stored as NULL tenant_id, callable against any tenant); a non-empty TenantID scopes the key to that single tenant.
type KeyStore ¶
type KeyStore interface {
// Issue mints a new key, persists its hash, and returns the plaintext once.
Issue(ctx context.Context, spec KeySpec) (IssuedKey, error)
// IssueSession mints a multi-tenant, manage-keys session token that
// expires after ttl. It is the foundation for dashboard login: a session
// is just an expiring KeyStore row, validated by the same middleware path
// as any other API key.
IssueSession(ctx context.Context, ttl time.Duration) (IssuedKey, error)
// Lookup resolves a key by its sha256 hex hash with NO tenant scoping.
// Revoked keys still return (found=true): revocation is enforced by the
// middleware, not by hiding the row.
Lookup(ctx context.Context, keyHash string) (rec KeyRecord, found bool, err error)
// List returns every key as a redacted KeyRecord (no hash/plaintext).
List(ctx context.Context) ([]KeyRecord, error)
// Revoke stamps revoked_at on the key. The row stays visible to Lookup.
// Revoking an already revoked key succeeds and keeps the first revoked_at;
// an unknown id returns ErrKeyNotFound.
Revoke(ctx context.Context, id string) error
}
KeyStore is the persistence port for hosted-fabriq API keys. It operates on the instance-global fabriq_api_key table (created by migration 0027), which is deliberately NOT under RLS: keys are resolved by hash before any tenant context exists, mirroring the outbox's tenant-less rationale.
func NewKeyStore ¶
NewKeyStore builds a KeyStore over a grove.DB. The grove MUST be backed by the pg driver (grove + pgdriver), which is fabriq's only supported backend; this mirrors postgres.OpenWithGrove's driver assertion.
type KeyToucher ¶ added in v1.9.0
type KeyToucher interface {
// Touch records at as the key's last use. An unknown id is not an error.
Touch(ctx context.Context, id string, at time.Time) error
}
KeyToucher is an optional KeyStore capability: a store that implements it gets the id of every key that authenticates a request, so it can record the last use. It is type-asserted by the authn middleware rather than added to KeyStore, so existing KeyStore implementations keep compiling and a store without it simply never reports a last use.
type Option ¶
type Option func(*config)
Option configures the adminapi extension.
func WithAdminLogin ¶
WithAdminLogin enables the dashboard-login surface (POST {BasePath}/login + /logout): username is compared verbatim at login time and password is bcrypt-hashed immediately, at option-application time — the plaintext password is never retained. WithAdminLogin REQUIRES WithAuth: login mints a session token via KeyStore.IssueSession, so Extension.Start fails fast with an error if AdminLoginUser is set but no KeyStore was configured.
func WithAnalyticsAdmin ¶ added in v0.0.6
func WithAnalyticsAdmin() Option
WithAnalyticsAdmin enables the cross-tenant analytics sink endpoints — synchronous backfill (single-tenant or fleet) plus a status check (the HTTP twin of `fabriq analytics backfill`). Leave OFF unless the host guards the admin API: backfill replays tenant snapshots into the analytics sink and is an instance-global operation.
func WithAnalyticsRead ¶ added in v0.0.6
func WithAnalyticsRead() Option
WithAnalyticsRead enables the read-only analytics status/freshness endpoint (GET /analytics/status) without the mutating operations — a least-privilege gate for a freshness dashboard. WithAnalyticsAdmin implies this.
func WithAuth ¶
WithAuth sets the instance-global API key store, enabling the /admin/keys issue/list/revoke management routes (registered only when the store is non-nil). It does NOT install the request-verifying middleware — that is a separate, explicit step: attach authMiddleware(store, basePath) via WithRouteOptions to actually gate admin requests on a valid key.
func WithAuthDisabled ¶
func WithAuthDisabled() Option
WithAuthDisabled turns OFF secure-by-default auth: no KeyStore is auto-provisioned and the admin API is served without authentication. Use for trusted internal deployments or tests that intentionally run keyless.
func WithAuthRequired ¶ added in v1.9.0
func WithAuthRequired() Option
WithAuthRequired makes auth fail closed for a hosted server. Register returns an error when no KeyStore can be provisioned (no WithAuth and no reachable Postgres DB) or when WithAuthDisabled is also set, instead of serving the admin API unauthenticated. Start then requires FABRIQ_ADMIN_KEY to hold a well-formed key (fq_ followed by at least 32 letters or digits) and never generates and logs a bootstrap key.
WithAuthRequired alone does not turn off the default admin/admin dashboard login. A hosted server passes WithoutDefaultLogin as well.
func WithAuthorizer ¶ added in v0.0.6
func WithAuthorizer(a Authorizer) Option
WithAuthorizer overrides the default flag-based capability gating with a host-supplied Authorizer — the seam for per-user RBAC (e.g. bridging warden). fabriq itself imports no authz system; the host wires the adapter.
func WithBasePath ¶
WithBasePath sets the admin API base path (default "/admin").
func WithConnectionsRead ¶ added in v0.0.6
func WithConnectionsRead() Option
WithConnectionsRead enables the connection/topology info endpoints — GET /connections (the tier's stores + cluster topology) and GET /tenants/:id/connection (a tenant's dedicated database). All connection info is REDACTED server-side: passwords and other secrets are never serialized. It is a read-only gate, deliberately separate from the mutating WithTenantsAdmin so a topology viewer can be granted without provisioning rights; leave OFF unless the host guards the admin API.
func WithEmbedder ¶
WithEmbedder supplies the embedding model used by the text-mode vector search endpoint (POST {BasePath}/search/vector with a {query} body). Fabriq stays model-agnostic: the host provides the implementation (Anthropic, OpenAI, a local model). When unset, text-mode vector search returns 501 and callers must use the similar-to-entity mode ({id} body) instead.
func WithOperatorOnlyInstanceOps ¶ added in v1.9.0
func WithOperatorOnlyInstanceOps() Option
WithOperatorOnlyInstanceOps limits the admin routes that reach past one tenant to operator keys, the ones with CanManageKeys. With it set, these answer 403 for any other key:
POST {BasePath}/query raw SQL (query.raw)
DELETE {BasePath}/schema/:type drop a dynamic type
DELETE {BasePath}/schema/:type/fields/:column drop a field
POST {BasePath}/schema/:type/rename-field rename a field
Raw SQL runs as the serving role, which can read instance tables that RLS does not fence (API keys, the tenant registry), and a dynamic type's table is shared by every tenant, so dropping or renaming it touches them all. Defining a type and adding fields stay open to any key. A request with no resolved key (auth off) is refused too, so the gate fails closed. A hosted server (fabriq serve) sets this; library defaults leave it off.
func WithRouteOptions ¶
func WithRouteOptions(opts ...forge.RouteOption) Option
WithRouteOptions forwards forge route options (auth middleware, OpenAPI decorators) to all admin routes — the extension stays auth-agnostic.
func WithSchemaAdmin ¶
func WithSchemaAdmin() Option
WithSchemaAdmin enables the privileged schema-ops endpoints — running/rolling back static migrations and executing ad-hoc DDL. These are instance-global, schema-owner operations; leave OFF unless the host guards the admin API.
func WithTenantRegistry ¶ added in v1.9.0
func WithTenantRegistry(r TenantRegistry) Option
WithTenantRegistry sets the static-mode tenant registry that serves POST/GET /tenants when the fabric runs without a catalog. With a registry set, POST /keys also refuses a tenantId that is not registered (404).
func WithTenantsAdmin ¶ added in v0.0.6
func WithTenantsAdmin() Option
WithTenantsAdmin enables the tenant-management endpoints — list/get plus the suspend/resume lifecycle (the HTTP twin of the `fabriq tenant` CLI). These are instance-global operations; leave OFF unless the host guards the admin API. Without a catalog, Register also provisions the static tenant registry (POST/GET /tenants), which makes POST /keys check tenantId against it.
func WithWritePolicy ¶
func WithWritePolicy(p agent.WritePolicy) Option
WithWritePolicy sets the agent-toolkit write allowlist backing the guarded write endpoint (POST {BasePath}/agent/remember). Deny-by-default: without this option Remember permits no writes. Example: allow product creates/updates via agent.WritePolicy{Allow: map[string][]command.Op{"product": {command.OpCreate, command.OpUpdate}}}.
func WithoutDefaultLogin ¶ added in v1.9.0
func WithoutDefaultLogin() Option
WithoutDefaultLogin stops secure-by-default auth from enabling the admin/admin dashboard login. Login stays off unless the host sets WithAdminLogin.
type TenantRecord ¶ added in v1.9.0
TenantRecord is one row of the static-mode tenant registry.
type TenantRegistry ¶ added in v1.9.0
type TenantRegistry interface {
// Create records the tenant. An existing id returns the stored record and
// created == false, with its label and created_at untouched.
Create(ctx context.Context, id, label string) (rec TenantRecord, created bool, err error)
// Get returns the tenant, or found == false when the id is unknown.
Get(ctx context.Context, id string) (rec TenantRecord, found bool, err error)
// List returns every tenant ordered by id.
List(ctx context.Context) ([]TenantRecord, error)
}
TenantRegistry is the persistence port for static-mode tenants. In row-level security mode a tenant is only a string on each row, so the registry is the one place that records a tenant exists. It operates on the instance-global fabriq_tenant table (created by migration 0034), which is deliberately NOT under RLS. Catalog mode has its own directory and does not use it.
func NewTenantRegistry ¶ added in v1.9.0
func NewTenantRegistry(gdb *grove.DB) TenantRegistry
NewTenantRegistry builds a TenantRegistry over a grove.DB. The grove MUST be backed by the pg driver, like NewKeyStore.
Source Files
¶
- agent_write.go
- analytics.go
- analytics_jobs.go
- analytics_query.go
- authn.go
- authn_middleware.go
- authz.go
- cache_admin.go
- capabilities.go
- commands.go
- connections.go
- controller.go
- crdt.go
- distill.go
- drift.go
- entities_write.go
- errors.go
- events.go
- extension.go
- files.go
- graph.go
- jobspan.go
- keys.go
- live.go
- login.go
- migrations.go
- plugins.go
- projections.go
- query.go
- recall.go
- schema.go
- schema_ddl.go
- schema_write.go
- search.go
- spatial.go
- tenants.go
- tenants_static.go
- timeseries.go
- vector_admin.go
- version.go