The content type API is built on a two-layer architecture: a dispatch engine that routes operations to handlers, and a REST API layer that translates HTTP into engine calls.
The engine routes ActionRequest objects to the appropriate handler based on content type:
HTTP Request → Router → ActionRequest → Engine.Dispatch() → Handler → ActionResult → JSON Response
Key files:
engine.go— Engine struct, Dispatch(), handler registry, type name resolutionhandler.go— Handler interface + ExecutableHandler + HTTPHandlerbuiltin_core.go— BuiltinCoreHandler for pub.polis.core operationsauth.go— API key generation, validation, revocationrender.go— Content-type-aware site render helper
The Engine struct holds:
- A map of bundle names to
Handlerimplementations - Type name resolution (short names like
post→ fully-qualifiedpub.polis.post) - Bundle introspection (which types exist, which actions each supports)
Engine.Dispatch(ctx, ActionRequest) finds the bundle owning the requested content type, resolves to the appropriate handler, and calls Handler.Handle().
type ActionRequest struct {
Action string // "create", "list", "bless", etc.
ContentType string // "pub.polis.post" (always fully-qualified after resolution)
Payload json.RawMessage // Action-specific input
}
type ActionResult struct {
Status string // "success" or "error"
Data interface{} // Action-specific output
}type Handler interface {
Handle(ctx context.Context, req ActionRequest, env HandlerEnv) (ActionResult, error)
Actions(contentType string) []string
}HandlerEnv provides the handler with site context (data directory, signing key, base URL, discovery client).
| Type | Description | Use Case |
|---|---|---|
builtin |
Go code in BuiltinCoreHandler | pub.polis.core types |
executable |
JSON stdin/stdout to external binary | Custom bundle with local script |
http |
JSON POST to URL | Custom bundle with remote service |
Bundles declare their handler type in bundle.json. The engine instantiates the appropriate handler at startup.
Handles all pub.polis.core content types by calling into cli-go/pkg/ packages:
pub.polis.post/create→publish.Publish()pub.polis.post/list→ readsindex.jsonlpub.polis.comment/create→blessing.Beseech()pub.polis.follow/list→following.Load()
Each operation is a method on BuiltinCoreHandler. New operations are wired by adding a case to the action dispatch switch.
For executable bundles, the handler:
- Serializes the
ActionRequestas JSON - Writes it to the executable's stdin
- Reads JSON from stdout
- Deserializes into
ActionResult
The executable path comes from bundle.json.
For http bundles, the handler:
- Serializes the
ActionRequestas JSON - POSTs it to the handler URL from
bundle.json - Reads the JSON response
- Deserializes into
ActionResult
Thin HTTP layer that translates REST conventions into ActionRequest objects:
router.go— Route registration, path parsing, auth enforcementhandlers.go— HTTP → Dispatch → JSON adapters, error mappingmiddleware.go— CORS, body size limits, auth middleware
| HTTP | Route | Action |
|---|---|---|
| GET | /v1/content/{type} |
list |
| GET | /v1/content/{type}/{id} |
get |
| POST | /v1/content/{type} |
create |
| PUT | /v1/content/{type}/{id} |
update |
| DELETE | /v1/content/{type}/{id} |
delete |
| POST | /v1/content/{type}/actions/{action} |
{action} |
| GET | /v1/content/{type}/drafts |
draft.list |
| GET | /v1/content/{type}/drafts/{id} |
draft.get |
| POST | /v1/content/{type}/drafts |
draft.save |
| DELETE | /v1/content/{type}/drafts/{id} |
draft.delete |
The auth middleware extracts the Bearer token from the Authorization header, hashes it with SHA-256, and checks against stored hashes in .polis/api-keys.json. GET requests on content and bundle routes bypass auth.
Engine errors are mapped to HTTP status codes:
| Engine Error | HTTP Status |
|---|---|
| Unknown content type | 404 |
| Unsupported action | 400 |
| Validation failure | 400 |
| Auth failure | 401/403 |
| Internal error | 500 |
API keys are managed via ops.GenerateAPIKey(), ops.ValidateAPIKey(), and ops.RevokeAPIKey():
- Keys are generated as random hex strings prefixed with
polis_ - Only SHA-256 hashes are stored (in
.polis/api-keys.json) - The plaintext key is returned exactly once at creation time
- Keys include a name and creation timestamp for management
To wire a new operation for an existing content type:
- Implement the operation in the appropriate
cli-go/pkg/package - Add a case to
BuiltinCoreHandler.Handle()inbuiltin_core.go - Add the action name to
BuiltinCoreHandler.Actions()for the content type - The REST routes already handle all standard CRUD + custom actions — no route changes needed
- Add tests in
builtin_core_test.go
To add a new content type via a third-party bundle:
- Create
content/<bundle>/bundle.jsondeclaring the type, handler, and actions - Implement the handler (executable script or HTTP endpoint)
- Register the bundle in
.well-known/polis - The engine discovers it at startup and routes actions automatically