Kernel Services Checklist
Complete inventory of ObjectStack kernel services with protocol methods, implementation status, and development requirements.
Kernel Services Checklist
This page is hand-maintained. The per-slot Provider column mirrors
CORE_SERVICE_PROVIDER (packages/spec/src/system/core-services.zod.ts), which
is the CI-guarded source of truth; the method inventories mirror the per-domain
contracts in packages/spec/src/api/protocol.zod.ts. When the two disagree,
the code wins. See Plugins & Packages for the full
package catalog.
The ObjectStack protocol defines 15 kernel services registered via the CoreServiceName enum (v17 removed the never-implemented graphql entry and retired the never-filled workflow slot, #4451). Each service maps to a set of protocol methods governed by its per-domain contract (DataProtocol, MetadataProtocol, ...) — the transitional ObjectStackProtocol composition alias was dissolved in v17 (ADR-0076 D9); capability availability comes from the runtime discovery services registry.
Key architecture principle: the kernel guarantees only data and metadata, and even those are filled by packages (@objectstack/objectql, @objectstack/metadata) rather than baked in — the kernel's own contribution is an in-memory fallback for the core slots that have one (metadata, cache, queue, job, i18n — not auth). Everything else — including auth and automation — is delivered by plugins. @objectstack/objectql is an example kernel implementation to get the basic API running; production kernels will be rebuilt as separate plugins.
Legend
- ✅ Implemented — the 17 kernel-provided protocol methods (
DataProtocol9 +MetadataProtocol8) - ⚠️ Framework — the slot is filled by the kernel's in-memory fallback: real reads and
writes, no persistence (self-declares
degraded, ADR-0076 D12) - ❌ Plugin Required — the remaining 33 methods declared across the other per-domain
contracts (
analytics2,automation1,packages6,views5,permissions3,realtime6,notification7,i18n3). Declared is not routed:packagesis answered kernel-side by the/packagesdispatcher domain over the ObjectQL registry,i18nhas a kernel in-memory fallback, andviews/permissionshave no implementation anywhere — the rest wait on whatever fills the slot. (workflow's 3 methods were the fourth such group; the whole slot retired in v17, #4451.)
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ Kernel Layer │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ metadata (✅) │ │ data (✅) │ │
│ │ MetadataPlugin │ │ ObjectQL example kernel │ │
│ │ → sys_metadata │ │ Will be rebuilt as plugins │ │
│ │ (in-mem fallback)│ │ │ │
│ └──────────────────┘ └──────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Plugin Layer │
│ All other services: analytics, auth, automation, │
│ ui, realtime, notification, ai, i18n, │
│ search, file-storage, cache, queue, job │
│ │
│ Discovery API reports availability per service │
│ (available / degraded / stub / unavailable) so │
│ clients adapt their UI accordingly │
└─────────────────────────────────────────────────────────┘Service Overview
| # | Service Name | Criticality | Methods | Status | Provider |
|---|---|---|---|---|---|
| 1 | metadata | core | 8 | ✅ Implemented (kernel fallback is ⚠️ in-memory) | @objectstack/metadata |
| 2 | data | required | 9 | ✅ Implemented | @objectstack/objectql |
| 3 | analytics | optional | 2 | ❌ Plugin Required | @objectstack/service-analytics |
| 4 | auth | core | — | ✅ Implemented | @objectstack/plugin-auth |
| 5 | ui | optional | 5 | ❌ Nothing fills this slot | @objectstack/metadata-protocol — /ui/view is served by its protocol service, not by a ui service |
| 6 | automation | optional | 1 | ❌ Plugin Required | @objectstack/service-automation |
| 7 | realtime | optional | 6 | ❌ Plugin Required (in-process only — no HTTP/WS route is mounted) | @objectstack/service-realtime |
| 8 | notification | optional | 7 | ❌ Plugin Required | @objectstack/service-messaging |
| 9 | ai | optional | — | ❌ Nothing ships in this repo | @objectstack/service-ai (Cloud/EE — not installable, so the table entry is null) |
| 10 | i18n | core | 3 | ✅ Built-in (in-memory fallback) | @objectstack/service-i18n |
| 11 | file-storage | optional | — | ❌ Plugin Required | @objectstack/service-storage |
| 12 | search | optional | — | ❌ Nothing ships | — |
| 13 | cache | core | — | ✅ Built-in (in-memory fallback) | @objectstack/service-cache |
| 14 | queue | core | — | ✅ Built-in (in-memory fallback) | @objectstack/service-queue |
| 15 | job | core | — | ✅ Built-in (in-memory fallback) | @objectstack/service-job |
The Provider column mirrors CORE_SERVICE_PROVIDER in
@objectstack/spec/system — the single table discovery reads when it tells a
caller what to install for an empty slot, and the value it reports as
provider. Two caveats. The table carries no entry for
metadata or data: those are filled by the engine itself, never by an
installable optional package, so they need no remedy (NO_REMEDY_SLOTS in the
guard script). And null there does not always mean "nothing ships" — it
means "no name belongs in an Install X sentence", which covers two cases:
- Nothing provides the slot at all —
search. Discovery says exactly that rather than naming a plausible package:No implementation ships for the '<slot>' slot — register a service under it to enable. (workflowand the never-realgraphqlsat here too until both were retired outright in v17, #4451 — a slot nothing fills and nothing consumes is better removed than explained.) - A provider exists but cannot be installed —
ai.@objectstack/service-airegisters the slot inobjectstack-ai/cloudand isprivate: true.
Two slots therefore override the generic line with a hand-written
REMEDY_DETAIL sentence: ui, because the package named does not fill the
slot (Served by the protocol service — register MetadataPlugin (@objectstack/metadata-protocol) to enable), and ai (Provided by @objectstack/service-ai in ObjectStack Cloud/Enterprise — no implementation ships in the open framework). scripts/check-service-providers.mjs fails CI
if a name in the table is not a real workspace package, or if a
CoreServiceName slot other than data/metadata is missing from it.
Criticality Levels
- required: System cannot start without this service
- core: Falls back to in-memory implementation with a warning if missing
- optional: Feature disabled; the API answers as an empty slot if missing (404, or the domain's 501)
1. metadata Service ✅ Implemented
Service Name: metadata · Criticality: core
Implementation: MetadataPlugin (@objectstack/metadata) — persisted to sys_metadata.
Fallbacks: the kernel's in-memory createMemoryMetadata (@objectstack/core) when nothing
registers the slot, over ObjectQL's in-memory SchemaRegistry of artifact-loaded metadata.
Route Mounts: /api/v1/meta, /api/v1, /api/v1/packages
Protocol Methods
| Method | Signature | Status |
|---|---|---|
getDiscovery | GetDiscoveryRequest → GetDiscoveryResponse | ✅ |
getMetaTypes | GetMetaTypesRequest → GetMetaTypesResponse | ✅ |
getMetaItems | GetMetaItemsRequest → GetMetaItemsResponse | ✅ |
getMetaItem | GetMetaItemRequest → GetMetaItemResponse | ✅ |
saveMetaItem | SaveMetaItemRequest → SaveMetaItemResponse | ✅ |
deleteMetaItem | DeleteMetaItemRequest → DeleteMetaItemResponse | ✅ |
getMetaItemCached | GetMetaItemCachedRequest → GetMetaItemCachedResponse | ✅ |
getUiView | GetUiViewRequest → GetUiViewResponse | ✅ |
Former Gaps — all now closed
Every gap this section used to list has shipped. Only the kernel's own
in-memory fallback is still a "framework": it serves real reads and writes but
never reaches disk, which is exactly what it self-declares (degraded).
| Former gap | Where it landed |
|---|---|
| DB Persistence | MetadataPlugin (@objectstack/metadata) persists to sys_metadata; only the kernel's in-memory fallback is non-persistent |
| Multi-instance Sync | MetadataClusterBridgePlugin (@objectstack/service-cluster) bridges the metadata.changed channel onto the cluster pub/sub bus, so a mutation on one node invalidates peer registry caches. Needs a cluster service and a metadata service exposing attachClusterPubSub() |
| Migration / Versioning | os diff <before> <after> compares two configurations and flags breaking changes; os migrate plan / os migrate apply reconcile the physical database against metadata |
| Hot Reload | MetadataPlugin watches its sources (watch) and the compiled artifact (artifactWatch) with chokidar; os dev rebuilds and the server reloads without a restart |
Discovery: Per-Service Status
The discovery endpoint returns a services map so clients know what is available.
The metadata entry is computed from the implementation that fills the slot, not
fixed (#4089): a stack running the kernel's in-memory fallback reports degraded with
that fallback's own message, while a stack with MetadataPlugin reports available and
no message. Below is the former — the fallback case:
{
"services": {
"metadata": {
"enabled": true, "status": "degraded", "handlerReady": true,
"route": "/api/v1/meta", "provider": "kernel",
"message": "In-memory metadata registry — real reads and writes, no persistence (lost on restart). Register MetadataPlugin for a persisted registry."
},
"data": {
"enabled": true, "status": "available",
"route": "/api/v1/data", "provider": "kernel"
},
"auth": {
"enabled": false, "status": "unavailable",
"message": "Install @objectstack/plugin-auth to enable"
}
}
}stub vs degraded — and what the dispatcher does with each
A service may self-declare that it is not the full thing (ADR-0076 D12), via a
__serviceInfo: { status, handlerReady?, message? } property on the registered
instance — see Services → presence is not capability for how
an in-process caller reads it. The two values mean different things, and for the
HTTP surface the difference is load-bearing (#4058):
| Declared | handlerReady default | Meaning | Dispatcher behaviour |
|---|---|---|---|
degraded | true | Really does the work, with reduced capability (in-memory, no persistence, no cross-process fan-out) | Served normally. Route advertised, requests handled. |
stub | false | Fabricates its answers — reports success for work that never happened | Treated as an EMPTY slot. Route not advertised, request answered exactly as if nothing were registered (404, or the domain's 501). |
Only handlerReady is consulted, never status — so an implementation that
declares degraded but explicitly sets handlerReady: false is also treated as
empty. The rule is enforced by one predicate (isServiceServeable) shared by
the service domains, the route-mount gate, and both discovery builders, so what
is advertised and what is served cannot disagree. It applies to the
dispatcher-owned domains: /analytics, /automation, /notifications, /ai,
/i18n. (/storage is no longer a dispatcher domain — the bridge was retired
in #4087 and service-storage mounts /api/v1/storage itself — but its route
advertisement is still gated on the same predicate.)
The services map still reports a registered stub as
{ enabled: true, status: "stub", handlerReady: false } rather than collapsing
it to unavailable — "something is in this slot, and it is a fake" says more
than "install a plugin".
2. data Service ✅ Implemented
Service Name: data · Criticality: required
Implementation: @objectstack/objectql → ObjectQL (IDataEngine)
Route Mount: /api/v1/data
@objectstack/objectql is an example kernel implementation to get the basic data API running. Future production kernels will be developed as separate plugins.
Protocol Methods
| Method | Signature | Status |
|---|---|---|
findData | FindDataRequest → FindDataResponse | ✅ |
getData | GetDataRequest → GetDataResponse | ✅ |
createData | CreateDataRequest → CreateDataResponse | ✅ |
updateData | UpdateDataRequest → UpdateDataResponse | ✅ |
deleteData | DeleteDataRequest → DeleteDataResponse | ✅ |
batchData | BatchDataRequest → BatchDataResponse | ✅ |
createManyData | CreateManyDataRequest → CreateManyDataResponse | ✅ |
updateManyData | UpdateManyDataRequest → UpdateManyDataResponse | ✅ |
deleteManyData | DeleteManyDataRequest → DeleteManyDataResponse | ✅ |
Driver Support
| Driver | Package | CRUD | Aggregation | Ready |
|---|---|---|---|---|
| InMemory | @objectstack/driver-memory | ✅ | ✅ | Dev/Test |
| PostgreSQL / MySQL / SQLite | @objectstack/driver-sql (Knex) | ✅ | ✅ | ✅ |
| MongoDB | @objectstack/driver-mongodb | ✅ | ✅ | ✅ |
| SQLite (WASM) | @objectstack/driver-sqlite-wasm | ✅ | ✅ | Browser/WebContainer |
3. analytics Service — Plugin Required
Service Name: analytics · Criticality: optional
Implementation: @objectstack/service-analytics (the only implementation)
Route Mount: /api/v1/analytics — only served when the plugin registers the service
The kernel-level "degraded analytics fallback" (a lightweight ObjectQL adapter
registered by the protocol assembly) was retired (#3891): it dropped the
caller's ExecutionContext — aggregates ran without RLS/tenant scoping — and
silently ignored the contract where filter. Without
@objectstack/service-analytics, /api/v1/analytics/* now answers 404 and
discovery reports analytics: { enabled: false, status: "unavailable" }.
This holds in dev mode too (#4000): plugin-dev no longer registers an
analytics dev stub, and the dispatcher treats a slot filled by any
self-declared stub (handlerReady: false, ADR-0076 D12) exactly like an empty
one — routes unmounted, request 404. To use analytics locally, install the real
engine; @objectstack/service-analytics works against the in-memory driver
(via its ObjectQL strategy, or InMemoryStrategy injected from
@objectstack/driver-memory), so it needs no database of its own.
And it is no longer analytics-only (#4058 step 2): every dispatcher-owned
domain now gates on handlerReady, so a slot occupied by a self-declared stub
— automation, notification, ai, wherever one is registered — answers as an
empty slot does. The implementations that really do the work in memory declare
degraded and keep serving. See
stub vs degraded.
Features (via @objectstack/service-analytics)
- Cube semantic queries: measures / dimensions /
wherefilter → SQL or ObjectQL aggregation - Context-aware read scoping: per-object RLS/tenant predicates resolved from the request's
ExecutionContext(fail-closed) - Cube metadata (
/analytics/meta), SQL preview (/analytics/sql), dataset queries (/analytics/dataset/query)
4. auth Service ✅ Implemented
Service Name: auth · Criticality: core
Implementation: AuthPlugin → AuthManager (@objectstack/plugin-auth, built on better-auth)
Route Mount: /api/v1/auth — only exposed when a plugin registers the auth service
The kernel does NOT handle auth. Install an auth plugin to enable authentication. Without it, the discovery response shows auth: { enabled: false, status: "unavailable" }.
Where each capability lives
The auth service covers authentication (identity). Authorization is not a
CoreServiceName slot at all — there is no permission service; the evaluators are
registered under their own names (security.permissions, security.rls,
security.fieldMasker) by @objectstack/plugin-security.
| Module | Area | Ships in |
|---|---|---|
| Identity Provider | Authentication | @objectstack/plugin-auth — better-auth sessions + jose JWTs (auth-manager.ts) |
| User CRUD | Authentication | @objectstack/plugin-auth — admin-user-endpoints.ts, admin-import-users.ts |
| Role Management | Authorization | @objectstack/plugin-security — owns role / permission-set / user-permission-set / role-permission-set objects (ADR-0029 K2) |
| Permission Engine | Authorization | @objectstack/plugin-security — security.permissions (object) + security.fieldMasker (field) |
| OAuth2 / OIDC | Authentication | @objectstack/plugin-auth — @better-auth/oauth-provider, @better-auth/sso |
| Sharing Rules | Authorization | @objectstack/plugin-sharing — sharing, sharingRules, shareLinks services |
| Row-Level Security | Authorization | @objectstack/plugin-security — security.rls (rls-compiler.ts) |
| Multi-tenancy | Authentication | @objectstack/plugin-auth — the tenancy service (tenancy-service.ts, ADR-0093/ADR-0105) |
| Territory access | Authorization | Not a separate module: expressed as an RLS dynamic-membership predicate (current_user.territory_account_ids, staged in ExecutionContext.rlsMembership) |
| SCIM | Authentication | @objectstack/plugin-auth — @better-auth/scim, gated by OS_SCIM_ENABLED |
Spec Files: identity/identity.zod.ts, identity/organization.zod.ts, security/permission.zod.ts, system/auth-config.zod.ts, system/tenant.zod.ts (all under packages/spec/src/)
5–6. Business Services
5. ui Service — 1 routed method ✅ (was 5 declared, none routed)
getUiView
Nothing anywhere registers the ui slot (#4093 / #4146), so CORE_SERVICE_PROVIDER.ui
names @objectstack/metadata-protocol rather than a ui plugin: the one route the
/ui domain serves is GET /api/v1/ui/view/:object[/:type], which calls getUiView
on the protocol service that assembleMetadataProtocol() registers (invoked by
ObjectQLPlugin, or by the standalone createMetadataProtocolPlugin()). Without it
the domain answers 501 with that remedy spelled out, not a generic "install a ui
plugin".
Retired in v17: ViewProtocol's five methods
listViews, getView, createView, updateView, deleteView — and their ten
Request/Response schemas — were removed in
#6239 under ADR-0049
enforce-or-remove. This checklist had recorded them as declared-and-unrouted since it
was written; the removal makes that reading permanent instead of re-derivable. Views
are read and written through the surfaces that always served them: the metadata API
(/api/v1/meta/view/:name, view being a metadata type) for the stored definition,
and getUiView above for the resolved render-time view.
The concrete cost of leaving it declared is on the record: #5948's issue body and its
2026-08-07 maintainer ruling both read GetViewResponseSchema — this retired block,
zero implementations — as the contract of GET /ui/view/:object/:type, whose declared
response is GetUiViewResponseSchema. One word apart, and identical to a grep.
Retired in v17: the workflow slot
The slot, its IWorkflowService contract and the three WorkflowProtocol
methods (getWorkflowConfig, getWorkflowState, workflowTransition) were
removed in #4451:
nothing ever registered or resolved the slot (ADR-0115 Evidence 5), no method
ever had an implementation, and no host ever mounted /api/v1/workflow. The
three capabilities it named are live elsewhere — state-machine transitions are
an object validation rule of type state_machine, approvals are approval
flow nodes on the approvals runtime (ADR-0019 — decisions via
POST /api/v1/approvals/requests/:id/{approve,reject,recall}, served by
@objectstack/plugin-approvals), and record-triggered automation is lifecycle
hooks + record_change flows.
6. automation Service — 1 method ✅ @objectstack/service-automation
triggerAutomation
Trigger engine, event triggers from ObjectQL hooks, flow executor, scheduled triggers.
The /automation dispatcher domain gates on isServiceServeable, so a slot filled by
a self-declared stub answers as an empty one.
7–10. Communication Services
7. realtime — 6 methods · @objectstack/service-realtime
realtimeConnect, realtimeDisconnect, realtimeSubscribe, realtimeUnsubscribe, setPresence, getPresence
service-realtime is an in-process pub/sub bus, not an HTTP/WS surface. The
dispatcher has no /realtime branch and no plugin mounts one, so routes.realtime
is never advertised — an advertised route would 404 (ADR-0076 D12, #2462), and
features.websockets is hardcoded false for the same reason. These six
RealtimeProtocol members are declared and unrouted; when the service is registered
both discovery builders report the slot degraded with a message saying the bus is
in-process only and no HTTP/WS surface is mounted. Re-advertising waits on a real
transport.
8. notification — 7 methods · @objectstack/service-messaging
registerDevice, unregisterDevice, getNotificationPreferences, updateNotificationPreferences, listNotifications, markNotificationsRead, markAllNotificationsRead
The slot name is notification (singular) and the package that fills it shares no word
with it — which is why the remedy sentence is looked up in CORE_SERVICE_PROVIDER
rather than derived from the slot name. The /notifications domain currently routes
the inbox subset: GET /notifications → listInbox, POST /notifications/read →
markRead, POST /notifications/read/all → markAllRead. All three are optional
on INotificationService — a send-only provider (SMTP, Twilio, a webhook) fills the
slot legitimately without an inbox, and each route probes its own method and answers
501 when absent.
9. ai — contract removed ❌
aiNlq, aiSuggest, aiInsights
Removed. These three were optional protocol methods (aiNlq? …) that no
service in any repo ever implemented — the /ai/{nlq,suggest,insights} routes
they backed were mounted by nothing and 404ed for their whole life. In v17 the
AiProtocol contract, its schemas, and the client.ai.{nlq,suggest,insights}
methods that called them were deleted outright (#3718). The AI service that
does exist (service-ai, Cloud/EE) serves a different surface — chat,
complete, models, conversations, agents — and client.ai was rebuilt against
those real routes, so this entry describes a contract that no longer exists.
The ai slot still exists in CoreServiceName, but nothing in this repo fills
it (CORE_SERVICE_PROVIDER.ai is null).
10. i18n — 3 methods
getLocales, getTranslations, getFieldLabels
Service Name: i18n · Criticality: core
Implementations: @objectstack/service-i18n (production — file-based) · In-memory fallback (createMemoryI18n from @objectstack/core)
Route Mount: /api/v1/i18n
Contract: II18nService in @objectstack/spec/contracts
Service Registration
| Environment | Provider | Registration |
|---|---|---|
| Production | I18nServicePlugin | File-based FileI18nAdapter loads JSON locale files from disk |
| In-memory fallback | @objectstack/core (createMemoryI18n) | Two paths, same factory: the kernel pre-injects it for any unfilled core slot (CORE_FALLBACK_FACTORIES), and AppPlugin registers it during start when the stack declares translation bundles and no i18n service is present. Self-describes as degraded (ADR-0076 D12) |
| Development | DevPlugin | Auto-wires I18nServicePlugin when the stack declares translations; otherwise the AppPlugin fallback above applies. DevPlugin registers no stub of its own (ADR-0115) |
// Production (real service)
kernel.use(new I18nServicePlugin({ defaultLocale: 'en', localesDir: './i18n' }));
// Development (automatic — DevPlugin wires I18nServicePlugin when translations are declared)
kernel.use(new DevPlugin());Discovery & Handler Consistency
The Discovery API (/api/v1 or /.well-known/objectstack) and the i18n route handler (/api/v1/i18n/*) both use the same async resolution chain to detect i18n availability:
getServiceAsync() → getService() → context.getService() → services MapThis ensures that discovery.services.i18n.status always matches the actual runtime behavior — a service registered via any mechanism (sync Map, async factory, or context) will be reported correctly in both places.
The locale field in the discovery response is populated from the actual i18n service:
locale.default— fromi18nService.getDefaultLocale()(falls back to'en')locale.supported— fromi18nService.getLocales()(falls back to[default])
AppPlugin Auto-Loading
When an app bundle includes an i18n config and translations array, AppPlugin automatically loads the translation data into the i18n service during the start phase:
export default defineStack({
manifest: { id: 'com.example.crm', namespace: 'crm' },
i18n: { defaultLocale: 'en', supportedLocales: ['en', 'zh-CN'] },
translations: [CrmTranslations], // TranslationBundle[]
});AppPlugin will:
- Set the default locale via
i18nService.setDefaultLocale() - Call
i18nService.loadTranslations(locale, data)for each locale in every bundle - Skip gracefully if no i18n service is registered (no errors, just a debug log)
REST API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/i18n/locales | List available locales |
GET | /api/v1/i18n/translations/:locale | Get all translations for a locale |
GET | /api/v1/i18n/labels/:object/:locale | Get translated field labels for an object |
11–15. Infrastructure Services
cache, queue, and job are core services: like i18n, the kernel auto-injects an in-memory fallback when no plugin registers them (see CORE_FALLBACK_FACTORIES in packages/core/src/fallbacks/). The optional services (file-storage, search) stay disabled until a plugin provides them.
| Service | Description |
|---|---|
| file-storage | Unified upload/download/delete via @objectstack/service-storage, which mounts /api/v1/storage itself. Adapters: local FS and S3 (the S3 adapter's endpoint + path-style options cover S3-compatible services such as MinIO and R2). |
| search | Nothing ships. ISearchService and the engine enum (elasticsearch, meilisearch, …) exist in @objectstack/spec, but no package implements the contract or registers the search slot, so CORE_SERVICE_PROVIDER.search is null. |
| cache | General-purpose cache. In-memory fallback; memory or Redis adapter via @objectstack/service-cache. |
| queue | Message queue. In-memory fallback; durable DB-backed adapter (sys_job_queue) via @objectstack/service-queue (no BullMQ/Redis adapter is shipped). |
| job | Scheduled task execution via @objectstack/service-job. In-memory fallback; interval, cron, and DB-backed adapters with concurrency policy. |
Roadmap Status
The three-phase build-out this page used to propose has landed, under package names
that are service-* more often than plugin-*. Use the real names — a remedy naming
a package that cannot be installed is a dead end, which is why
CORE_SERVICE_PROVIDER is CI-checked against the workspace.
Shipped
| Capability | Package |
|---|---|
| Identity engine — sessions, JWT, user CRUD, OAuth2/OIDC, SCIM | @objectstack/plugin-auth |
| RBAC, field-level permissions, RLS | @objectstack/plugin-security |
| Record sharing + share links | @objectstack/plugin-sharing |
Metadata persistence (sys_metadata), HMR, cluster cache invalidation | @objectstack/metadata · @objectstack/service-cluster |
| Cache — memory + Redis adapters | @objectstack/service-cache |
| Flow orchestration, triggers, approval-node pauses | @objectstack/service-automation |
| Internationalization | @objectstack/service-i18n |
| File storage — local FS + S3 | @objectstack/service-storage |
| DB drivers — PostgreSQL / MySQL / SQLite (Knex), MongoDB, SQLite-WASM, in-memory | @objectstack/driver-sql · driver-mongodb · driver-sqlite-wasm · driver-memory |
| Notification engine + inbox | @objectstack/service-messaging |
| Scheduled tasks — interval, cron, DB-backed | @objectstack/service-job |
| Message queue — memory + durable DB-backed | @objectstack/service-queue |
| Realtime pub/sub (in-process; no HTTP/WS surface yet) | @objectstack/service-realtime |
Still open
| Slot | State |
|---|---|
| ui | Nothing registers the slot. ViewProtocol's five declared-and-unrouted methods were retired in v17 (#6239); view CRUD runs through /api/v1/meta, and /api/v1/ui/view/:object is served by the protocol service. |
| search | Nothing ships. Contract and engine enum exist in @objectstack/spec only. |
| ai | Nothing in this repo — service-ai (chat, completion, models, conversations) is Cloud/EE. |
| realtime transport | The service exists but no WebSocket/SSE route is mounted, so routes.realtime is deliberately never advertised. |
The workflow slot used to sit in this table ("nothing ships, no consumer").
It was retired outright in v17 (#4451, per ADR-0115 Evidence 5): the
capability lives in state_machine validation rules, approval flow nodes
(ADR-0019) and record_change flows, so there is nothing left for a slot to
promise.
Plugin Implementation Pattern
import type { Plugin } from '@objectstack/core';
import type { IDataEngine } from '@objectstack/spec/contracts';
let authService: AuthServiceImpl;
export const authPlugin: Plugin = {
// Plugin names are reverse-domain (e.g. 'com.objectstack.auth'),
// not the npm package name.
name: 'com.example.auth',
// ADR-0116 — declare what init() needs and what it unconditionally
// registers, so a misordering is a named boot error instead of a crash.
requiresServices: ['data'],
providesServices: ['auth'],
async init(ctx) {
// Register the 'auth' CoreServiceName value
const engine = ctx.getService<IDataEngine>('data');
authService = new AuthServiceImpl(engine);
ctx.registerService('auth', authService);
},
async start(ctx) {
// Late-bind anything that must wait for every plugin to be up.
// (There is no `onReady()` on IAuthService — that is your own type's API.)
ctx.hook('kernel:ready', async () => {
await authService.connect();
});
},
// destroy() takes no arguments — capture what you need in module scope
async destroy() {
await authService.shutdown();
},
};When a plugin registers a service, the discovery endpoint automatically updates:
services.auth.enabled→true,status→"available",handlerReady→true,route→"/api/v1/auth"(unless the instance self-declaresstub/degradedvia__serviceInfo, which is reported verbatim instead)routes.auth→"/api/v1/auth"appears in routesfeaturesflags follow for the slots that have one —search,files,analytics,ai,workflow,notifications,i18n(websocketsis hardcodedfalse; there is nofeatures.auth)