Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/retire-the-dev-stub-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'@objectstack/plugin-dev': minor
---

feat(plugin-dev)!: the stub table is retired — DevPlugin assembles real plugins and registers no service implementations of its own (ADR-0115, #4093, #4104).

DevPlugin used to fill every core-service slot no real plugin occupied with a dev stub. Every one of those stubs is gone. A slot nothing fills now stays EMPTY, exactly as in production: routes answer 404/501, discovery reports `unavailable`, and in-process consumers must handle absence — which production already required of them. FROM → TO per retired slot:

| Slot | The stub did | Instead |
|:---|:---|:---|
| `security.permissions` | allow-all `checkObjectPermission()` | install `@objectstack/plugin-security` (already part of the default assembly) |
| `security.rls` | compiled no row filter | same — `plugin-security` |
| `security.fieldMasker` | returned results unmasked | same — `plugin-security` |
| `auth` | `verify()` accepted everyone as admin | install `@objectstack/plugin-auth` (already part of the default assembly) |
| `data` | accepted writes, stored nothing | install `@objectstack/objectql` (already part of the default assembly) |
| `ui` | shapeless `{}` placeholder | nothing consumed it; handle the absent slot |
| `ai` | placeholder chat/complete answers | install a real AI service |
| `automation` | `execute()` reported success without running | install an automation engine plugin |
| `notification` | claimed "sent", delivered nothing | install a notification service |
| `file-storage` | in-memory files lost on restart | `@objectstack/service-storage` — now auto-wired by DevPlugin when installed (local-disk adapter) |
| `realtime` | in-process pub/sub copy | `@objectstack/service-realtime` — now auto-wired by DevPlugin when installed (its default in-memory adapter) |
| `search` | in-memory substring index | no consumer resolves this slot; a future search service ships its own dev strategy |
| `workflow` | unvalidated state transitions | no consumer resolves this slot; a future workflow service ships its own dev strategy |
| `metadata` | a second hand-written copy of core's `createMemoryMetadata` | no behavior change — the kernel pre-injects core's fallback for empty core slots (`CORE_FALLBACK_FACTORIES`), and ObjectQL registers the real metadata service in the default assembly |
| `cache` / `queue` / `job` / `i18n` | re-registered core's `createMemory*` fallbacks | no behavior change — the kernel pre-injects the same core fallbacks automatically; install `@objectstack/service-cache` / `service-queue` / `service-job` for real engines, and i18n auto-wires from the stack's translations (unchanged) |

Also new, from the same ADR:

- **Production guard** (first shipped with the security-trio subset): `DevPlugin.init()` throws when `NODE_ENV === 'production'` — the assembly is built around a well-known default auth secret and a seeded dev admin. Escape hatch: `OS_ALLOW_DEV_PLUGIN=1`.
- **Assembly auto-wire**: `@objectstack/service-storage` and `@objectstack/service-realtime` are wired as optional child plugins when installed (both ship with DevPlugin's dependencies), so dev keeps working file storage and realtime through real implementations.
- `options.services` keys for the retired stubs are accepted and ignored; `'file-storage'` / `'realtime'` now toggle the real service wiring.

One-line fix for an upgrading stack: if something you called in dev now throws "service not found" or 404s, that call was consuming a fabricated answer — install the real service for that slot (table above), or make the caller tolerate absence the way it already must in production.
16 changes: 5 additions & 11 deletions content/docs/kernel/services-checklist.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -310,30 +310,24 @@ methods, so this entry describes a contract with no implementer on either side.
`getLocales`, `getTranslations`, `getFieldLabels`

**Service Name**: `i18n` · **Criticality**: `core`
**Implementations**: `@objectstack/service-i18n` (production — file-based) · Built-in in-memory fallback · Dev Plugin (in-memory stub)
**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

The i18n service is a **core built-in service** with automatic in-memory fallback. If no plugin registers the service, the kernel automatically injects an in-memory implementation during startup:

| Environment | Provider | Registration |
|:------------|:---------|:-------------|
| **Production** | `I18nServicePlugin` | File-based `FileI18nAdapter` loads JSON locale files from disk |
| **Built-in Fallback** | Kernel | In-memory Map-backed stub, auto-injected when no plugin provides `i18n` |
| **Development** | `DevPlugin` | In-memory Map-backed stub, supports `loadTranslations()` |
| **In-memory fallback** | `@objectstack/core` (`createMemoryI18n`) | Registered by `AppPlugin` 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) |

```typescript
// Production (overrides built-in fallback)
// Production (real service)
kernel.use(new I18nServicePlugin({ defaultLocale: 'en', localesDir: './i18n' }));

// Development (automatic — DevPlugin registers i18n stub for all 16 core services)
// Development (automatic — DevPlugin wires I18nServicePlugin when translations are declared)
kernel.use(new DevPlugin());

// No plugin required — kernel auto-injects in-memory fallback if none registered
const kernel = new ObjectKernel();
await kernel.bootstrap(); // i18n service available via built-in fallback
```

#### Discovery & Handler Consistency
Expand Down
6 changes: 3 additions & 3 deletions content/docs/plugins/packages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,10 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern

### @objectstack/plugin-dev

**Local Development Plugin** — one-line local stack: wires ObjectQL, the in-memory driver, auth, security, the HTTP server, REST and the dispatcher, then fills any still-unclaimed core service slot with an in-memory dev implementation.
**Development Assembly Plugin** — one plugin that wires the real platform stack for zero-config local development.

- **Features**: Composes the real plugins above; registers dev implementations for unclaimed slots, each declaring what kind of fake it is (`__serviceInfo` — `degraded` when it really does the work in memory, `stub` when it fabricates). Slots that would fabricate an answer over HTTP get no implementation at all: `analytics` and the three `security.*` handles stay empty on purpose.
- **When to use**: Local development only. **`init()` throws under `NODE_ENV=production`** — set `OS_ALLOW_DEV_PLUGIN=1` only if you deliberately want the dev slate under a production `NODE_ENV`.
- **Features**: Auto-assembles ObjectQL + in-memory driver + auth + security + Hono server + REST + dispatcher + app metadata, plus optional real services when installed (storage, realtime, i18n); registers no stubs — a slot no plugin fills stays empty, as in production (ADR-0115); refuses to boot with `NODE_ENV=production` (`OS_ALLOW_DEV_PLUGIN` escape hatch)
- **When to use**: Zero-config local development and playgrounds
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/plugins/plugin-dev/README.md)

### @objectstack/plugin-approvals
Expand Down
11 changes: 10 additions & 1 deletion docs/adr/0115-plugin-dev-assembly-not-stub-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Precision, for the record: the `auth` stub is **dead code on the real identity p

**D5 — Direct cut inside the 17.x rc window; the changeset carries the FROM → TO per slot.** No deprecation window and no stub-preserving grace release: the honest `stub`/`degraded` self-descriptions shipped by #4082/#4086 already are the deprecation notice (discovery has been telling every consumer these are not capabilities), and keeping an allow-all security fake alive one extra release "to be polite" is exactly what D2 forbids. External rc consumers get the standard breaking-changeset prescription (Post-Task Checklist rule: FROM → TO and the one-line fix — *install the corresponding real service, or handle the absent slot as production already requires*). Resolves open question 1.

**D6 — plugin-dev grows a production guard, in the same change as Tier A.** `DevPlugin.init` throws when `NODE_ENV === 'production'`, escape hatch `OS_ALLOW_DEV_PLUGIN_IN_PRODUCTION=1` (Prime Directive #9 `OS_ALLOW_*` shape — deliberately scary). Deleting the fakes removes the security-semantics hazard; the guard covers what deletion cannot: the plugin still assembles a stack around a well-known default auth secret and a seeded dev admin, which no production deployment should get by accident. It does not ship as a separate earlier fix because Tier A itself waits for nothing (resolves open question 3 — the "maybe it shouldn't wait" concern was about the security stubs, and D2 removes them rather than gating them).
**D6 — plugin-dev grows a production guard, in the same change as Tier A.** `DevPlugin.init` throws when `NODE_ENV === 'production'`, escape hatch `OS_ALLOW_DEV_PLUGIN=1` (Prime Directive #9 `OS_ALLOW_*` shape — deliberately scary; the shipped name, landed by #4126's subset). Deleting the fakes removes the security-semantics hazard; the guard covers what deletion cannot: the plugin still assembles a stack around a well-known default auth secret and a seeded dev admin, which no production deployment should get by accident. It does not ship as a separate earlier fix because Tier A itself waits for nothing (resolves open question 3 — the "maybe it shouldn't wait" concern was about the security stubs, and D2 removes them rather than gating them).

**D7 — The descriptions converge on what the plugin is.** `content/docs/plugins/packages.mdx` and the plugin README/class doc describe an assembly plugin ("auto-wires ObjectQL + driver-memory + auth + security + HTTP server + REST + dispatcher + app metadata for local development"); the "Stub services" section and the "simulating all 17+ kernel services" claim are removed. Docs describing the retired design are ADR-0078's silent lie in prose form.

Expand All @@ -108,6 +108,15 @@ Sequenced to avoid editing `dev-plugin.ts` under an in-flight PR. Ratified by th

End state, mechanically checkable: `DEV_STUB_FACTORIES`, `DEV_STUB_SELF_INFO`, `SHAPELESS_STUB_SELF_INFO`, `NO_DEV_STUB_SERVICES`, and the fill loop no longer exist in `dev-plugin.ts`; `grep -r "createMemoryMetadata\|registerInMemory" packages/plugins/plugin-dev` is empty. The two-PR split is the ratified shape; the tier boundaries remain the decision.

### Update (2026-07-30, post-#4086 reconciliation): executed as ONE PR

Three facts changed between ratification and execution, all shrinking the work:

1. **#4086's merged form deleted nothing.** It gated the six dispatcher domains on `handlerReady` and explicitly deferred "should the fabricators keep occupying slots" to this ADR's Tier A. So `ai` / `automation` / `notification` join Tier A's deletion list here — the Context table's "retired" row described their HTTP surface (gated to empty-slot answers since #4086), not their registration.
2. **#4089 closed at the source.** #4082 had already given core's five fallbacks their `__serviceInfo` self-descriptions, so Tier B's core half was done before PR-2 existed; what remained of Tier B was only plugin-dev's `metadata` copy and the four wrappers.
3. **With the core half gone, the PR-1/PR-2 boundary lost its reason** (Tier B no longer reached into core), and the maintainer directed completing all remaining work at once. The implementation therefore landed as one PR: Tiers A+B+C, the D6 guard, the D4 assembly auto-wire, and the D7 docs convergence — the full end state above, under a single FROM → TO changeset narrative.
4. **#4126 landed the D2 security trio + D6 guard as a first subset while the full PR was in flight.** Its choices are canonical where they overlap: the escape hatch is `OS_ALLOW_DEV_PLUGIN` (not the longer name this ADR first wrote), the guard is a module-level `assertNotProduction()`, and an empty security slot gets one loud boot-log warn ("RBAC, row-level security and field masking are NOT enforced") instead of silence. It also filed #4113 — the dispatcher's `/auth` domain carries its own mock fallback in `packages/runtime` — as the remaining fabricator OUTSIDE plugin-dev; retiring plugin-dev's `auth` stub neither worsens nor fixes that path (both the stub and the runtime mock fabricate a 200; neither yields a session the identity resolver accepts), so #4113 stays the one place that class of fake survives.

## Consequences

- **+** "The capability is present" has one meaning across dev and production; the D12 producer inventory for plugin-dev closes completely instead of shrinking by one per issue.
Expand Down
44 changes: 16 additions & 28 deletions packages/plugins/plugin-dev/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
# @objectstack/plugin-dev

> Development Mode Plugin for ObjectStack — auto-enables **all 17+ kernel services** for a full-featured API development environment.
> Development Assembly Plugin for ObjectStack — wires the **real** platform stack for zero-config local development.

## Overview

Instead of manually wiring up ObjectQL, drivers, auth, HTTP server, REST endpoints, dispatcher, security, and metadata for local development, use `DevPlugin` to get a fully functional stack in one line.

The dev environment simulates **all kernel services** so you can:
- CRUD business objects via REST API
- Read, modify, and save views/apps/dashboards via metadata API (`PUT /api/v1/meta/:type/:name`)
- Use GraphQL, analytics, storage, and automation endpoints
- Authenticate with dev credentials (no real auth provider needed)
- Test UI permissions, workflows, and notifications with dev stubs
Everything it wires is a **real implementation** — there are no simulated services. A capability whose package is not installed is simply absent, exactly as in production: its routes answer 404/501 and discovery reports it `unavailable` (ADR-0115). That keeps "the capability is present" meaning the same thing in dev and production.

## Usage

Expand Down Expand Up @@ -53,16 +48,14 @@ plugins: [
port: 4000,
seedAdminUser: true,
services: {
auth: false, // Skip auth for quick prototyping
dispatcher: false, // Skip extended API routes
dispatcher: false, // Skip extended API routes
'file-storage': false, // Skip the storage service
},
}),
]
```

## What it auto-configures

### Real plugin implementations
## What it assembles (all real implementations)

| Service | Package | Description |
|---------|---------|-------------|
Expand All @@ -73,27 +66,22 @@ plugins: [
| Security | `@objectstack/plugin-security` | RBAC, RLS, field-level masking |
| Hono Server | `@objectstack/plugin-hono-server` | HTTP server on configured port |
| REST API | `@objectstack/rest` | Auto-generated CRUD + metadata endpoints |
| Dispatcher | `@objectstack/runtime` | Auth routes, GraphQL, analytics, packages, storage |

### ⛔ Local development only

`DevPlugin.init()` **throws under `NODE_ENV=production`**. It fills unclaimed service slots with fakes — some of which report success for work they never did — so a production process must not load it. Remove it from that deployment's plugin list and install the real services. `OS_ALLOW_DEV_PLUGIN=1` overrides the refusal if you deliberately want the dev slate under a production `NODE_ENV` (a staging box mimicking prod, a smoke test that pins the variable).
| Dispatcher | `@objectstack/runtime` | Auth routes, GraphQL, packages, storage bridges |
| Storage | `@objectstack/service-storage` | `file-storage` service (local-disk adapter, files under `./storage`) |
| Realtime | `@objectstack/service-realtime` | `realtime` service (in-memory adapter) |
| I18n | `@objectstack/service-i18n` | Auto-registered when the stack declares translations |

### Dev stubs (in-memory / no-op)
Every part is loaded via dynamic import and skipped with a log line when its package is not installed, and each can be disabled via `options.services`.

Most core kernel services not provided by a real plugin are registered as a dev stub, so the kernel service map is populated and callers get correct return types instead of `undefined`. Each one declares what kind of fake it is (`__serviceInfo`, ADR-0076 D12) — consumers, the dispatcher included, gate on that:
## Empty slots stay empty

| Class | Slots | Meaning |
|:---|:---|:---|
| `degraded` | `cache`, `queue`, `job`, `file-storage`, `search`, `realtime`, `i18n`, `workflow`, `metadata` | Really does the work, in memory only. Served normally over HTTP. |
| `stub` | `data`, `auth`, plus `ui` (placeholder with no implementation) | Fabricates its answer. Reported as a stub in discovery, and every dispatcher-owned domain answers it exactly as it answers an empty slot. |
This plugin registers **no service implementations of its own**. The earlier design filled every unoccupied kernel-service slot with a dev stub — fabricated answers such as allow-all permission checks and "sent" notifications that were never delivered. That design is retired (ADR-0115): a slot no real plugin fills stays empty, and consumers handle absence exactly as they already must in production.

**Never stubbed** — these slots stay empty on purpose, which is what production has when the real plugin isn't installed:
To use a capability locally, install its real service — e.g. `@objectstack/service-analytics` for `/analytics` (it runs an InMemory strategy), `@objectstack/service-cache` / `service-queue` / `service-job` for cache/queue/job.

- `analytics` (#4000). Install `@objectstack/service-analytics` (it runs an InMemory strategy).
- `security.permissions`, `security.rls`, `security.fieldMasker` (#4093). The former stubs answered "allowed" for every permission check, compiled no row-level filter, and returned rows unmasked — inverting the decisions they stood in for. ADR-0076 D12's rule is that a fallback may degrade features, **never security semantics**. Without `@objectstack/plugin-security` nothing enforces RBAC, RLS or field masking, and the boot log says so rather than a fake quietly saying yes.
## Production guard

All services are **optional** — if a peer package isn't installed it is skipped, and for the slots above a stub takes its place.
`init()` throws when `NODE_ENV === 'production'`: the assembly is built around a well-known default auth secret and a seeded dev admin. If you really mean it, set `OS_ALLOW_DEV_PLUGIN=1`.

## API Endpoints (when all services enabled)

Expand All @@ -120,7 +108,7 @@ All services are **optional** — if a peer package isn't installed it is skippe
| `authSecret` | `string` | dev default | JWT secret for auth sessions |
| `authBaseUrl` | `string` | `http://localhost:{port}` | Auth callback URL |
| `verbose` | `boolean` | `true` | Enable verbose logging |
| `services` | `Record<string, boolean>` | all `true` | Enable/disable individual services |
| `services` | `Record<string, boolean>` | all `true` | Enable/disable individual parts of the assembly |
| `extraPlugins` | `Plugin[]` | `[]` | Additional plugins to load |
| `stack` | `object` | — | Stack definition to load as project metadata |

Expand Down
4 changes: 3 additions & 1 deletion packages/plugins/plugin-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@objectstack/plugin-dev",
"version": "17.0.0-rc.0",
"license": "Apache-2.0",
"description": "Development Mode Plugin for ObjectStack — auto-enables all services with in-memory implementations",
"description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
Expand All @@ -27,6 +27,8 @@
"@objectstack/rest": "workspace:^",
"@objectstack/runtime": "workspace:^",
"@objectstack/service-i18n": "workspace:^",
"@objectstack/service-realtime": "workspace:^",
"@objectstack/service-storage": "workspace:^",
"@objectstack/setup": "workspace:^",
"@objectstack/spec": "workspace:*",
"@objectstack/types": "workspace:*"
Expand Down
Loading
Loading