plugin-api/ contains the small set of interfaces shared by core and plugins:
Principal, Event, AuditEvent, and BlobRef are boundary value types.
Business services, database helpers, and format-specific contracts remain in
core rather than expanding plugin-api.
Preserve filing ownership through these values: Event.SystemID and
AuditEvent.SystemID identify system/document work; zero is reserved for genuinely
instance-wide work. A documentless index refresh or approval proposal is still
system-owned. Principal.TokenSystemID is the authenticated token’s binding, not
the browser’s selected context; session principals leave it zero.
Application assembly
Compiled distributions can importgithub.com/johnnybravo-xyz/suchi/distro/app
and call app.Run(ctx, options). The caller resolves config.Config (normally
with config.LoadFile then config.Load), supplies its logger/build identity,
and owns signal cancellation and process exit codes. The community command uses
this same entry point. No copy of the command’s server wiring is required.
receiptAction is a compiled definition as described in
Automations. Options add to the built-ins;
duplicate kinds fail rather than replacing them. Migrations finish before the
registry is built. Core routes and subscribers are then assembled before
Configure runs exactly once. No dispatcher, watcher, scheduler, retention
loop, mail supervisor or HTTP listener has started at that point. A callback
error aborts startup and closes the database without starting runtime work. No
extension options are needed for a complete local archive.
PipelineProposalVersions is distribution release policy rather than mutable
server configuration. Zero fields create no new processing-update reminders.
A compiled distribution that opts in supplies checked-in per-pipeline
recommendation thresholds no greater than the pipeline revisions in that build;
thresholds newer than the compiled pipeline revisions stop startup. Keep this policy in the tagged source so
the same tag cannot produce artifacts with different proposal behavior.
Services contains only the instance’s DB, HTTP Mux, durable Jobs
dispatcher, Approvals engine, immutable Actions registry and Log. Compiled
distributions use it to add native routes and register durable subscribers.
The fields are instance-owned; registration in one assembled application does
not affect another. Configure routes pass through the normal middleware, but
they are absent from the explicit token allowlist and therefore remain
session-only. Route handlers still own their role, ACL and system checks.
MigrationSet holds a stable component name and the complete ordered
[]db.Migration history, starting at version 1 without gaps. Names match
[a-z][a-z0-9_-]{0,63}; core is reserved. SQL cannot be empty. Keep applied
SQL and RebuildTables unchanged; add a new version for changes. Each component
advances independently in _suchi_extension_migrations, with checksums and
application timestamps. Public core versions remain in PRAGMA user_version;
never append or renumber extension steps into that sequence. An unchanged rerun
is a no-op; drift or a binary missing an applied version fails before new steps
in that set. Omitting a component does not remove its stored schema/history.
Migration SQL and callbacks are trusted compiled code. Migration SQL must not
manage transactions, write user_version, or alter the reserved _suchi_ ledger
namespace. Each step uses the sole writer and records its ledger row atomically.
Rebuild migrations retain core’s foreign-key checking/restoration convention.
A failed step leaves earlier completed versions intact and rolls back its own
schema, data and applied record. See Storage and transaction invariants
and Runtime, jobs, and integrations for storage and boot
ownership.
Each extension owns the explicit tables declared by its migration set; there is
no generic core plugin_kv store. During a core compatibility table rebuild,
Suchi preserves extension-created indexes and triggers attached to core tables
by temporarily removing them and replaying their exact DDL. A dependency on a
retired core object aborts adoption atomically rather than dropping extension
data or schema.
This is one compile-time composition callback, not runtime plugin loading or a
general registration framework. It does not expose the CAS, API server or a
service lookup mechanism. The lower-level subscriber/authenticator/sink
contracts below remain explicit code dependencies.
Subscriber contract
Handle must be idempotent. Persist a
deduplication key or use a unique database constraint before applying an
observable side effect. Respect context cancellation and return failures so the
job can retry or become visible as dead.
Do network and subprocess work outside a database transaction. Open one short
write transaction only after the external result is available. State needed
after restart belongs in the database or event payload, not a package-level
cache.
Enqueue through jobs.Enqueue(ctx, tx, kind, docID, systemID, payload): the explicit
system must match any document. Zero persists as NULL only for global work.
The outbox inserts jobs without deduplicating them. Resolve metadata and background
actions within the event/document system, rechecking current referenced records
in the writer; a job is not an authenticated cross-system document-link grant.
Minimal shape:
Authenticator contract
(nil, nil) when the request does not use your authentication scheme so
the next authenticator can try. Return an error when your scheme is present but
invalid; silently falling through would allow an authentication downgrade.
An authenticator should resolve the external identity to a Suchi user and
return only the principal fields needed by authorization and audit. Put roles,
group membership, and object policy in the authorization layer rather than in
provider-specific token parsing.
Token authenticators must set TokenSystemID; an external token that leaves it
zero is restricted to original system 1, never all systems. Local tokens also
carry TokenID so mutations can recheck revocation, actor and binding inside the
single writer, plus the server-selected TokenSource issuance provenance.
Authentication does not grant membership or document permission; even
administrator tokens cannot escape their bound system or an explicit target.
Local issuance uses the caller-owned transaction contract
IssueAPIToken(ctx context.Context, tx *sql.Tx, userID, systemID int64, name, scopes, source string) (string, error).
The API’s TokenIssuer seam has the same signature. Keep pairing consumption,
current membership checks and issuance in that transaction so failure rolls back
consumption and membership removal cannot be bypassed by a later re-admission.
Register more specific authenticators before general local session and token
handling. See plugins/oidc and plugins/local-auth for shipped examples.
Audit sink contract
audit.RegisterSink. The local audit_events table remains the
durable record. Sink delivery is best effort and happens after the database
write, so Emit must be fast or hand work to its own bounded buffer. Never add
document content or plaintext credentials to an audit event.
Package and build wiring
Shipped plugins live in the root module. A compiled distribution assembles the plugins it includes throughapp.Options; integrations may use plugin-api and
narrowly required core packages for explicit database or sandbox behavior.
Rebuild and test the final binary after changing plugins. Dependencies become
part of that binary, so prefer the standard library and existing helpers.
Boundaries
- Do not rewrite original CAS blobs. Store a derived blob and update metadata.
- Use
core/sandboxfor external commands rather than rawos/exec. - Keep write transactions free of HTTP, model, and subprocess calls.
- Do not create a plugin interface for behavior owned by one shipped core package.
- Test subscribers directly with a migrated temporary database; test
authenticators with
httptestrequests.
plugins/local-auth, plugins/oidc,
plugins/llm-classifier, and core/pipeline/postingest.