You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There's no class hierarchy, no decorators, no DI container — just a typed object literal passed to `definePlugin()`. That's intentional: plain objects survive bundling well, are easy to test, and don't require any framework metadata.
107
114
108
-
The `ctx` object passed to `onBoot`is the gateway to everything: D1 (`ctx.db`), KV (`ctx.kv`), R2 (`ctx.r2`), the auth/content/media services, and a per-plugin logger. Plugins never reach into globals — every dependency is injected.
115
+
The `ctx` object passed to `onBoot`exposes `ctx.hooks` (typed event facade), `ctx.cap` (capability-gated services), `ctx.env` (runtime bindings — `ctx.env.DB`, `ctx.env.KV`, etc.), and `ctx.raw` (escape hatch to the underlying context). There is no `ctx.logger` — use `console.info` / `console.warn` or bring your own logger.
109
116
110
117
## Three Tiers of Plugins
111
118
@@ -150,7 +157,7 @@ In v3, `registerPlugins()` replaces the old `PluginManager` class. The flow look
150
157
└──────────────────────┘
151
158
```
152
159
153
-
`registerPlugins(plugins, app, ctx)` does the heavy lifting:
160
+
`registerPlugins(app, plugins, hostContext)` does the heavy lifting:
154
161
155
162
1.**Resolve dependencies** — topological sort over each plugin's `dependencies` array.
156
163
2.**Call `register(app)`** — each plugin mounts its routes and middleware onto the Hono app.
@@ -234,20 +241,28 @@ A few details that aren't obvious from the surface:
234
241
-**Recursion is detected.** The system tracks an `executing` set to prevent infinite loops if a hook handler triggers the same hook.
235
242
-**Errors are isolated.** A handler that throws is logged but doesn't abort the chain — unless its message contains `"CRITICAL"`, which short-circuits everything.
236
243
237
-
The standard hook names live in the exported `HOOKS` constant:
244
+
SonicJS has two hook surfaces: a **typed catalog** (preferred) and a **legacy hook bus** (raw, string-keyed):
245
+
246
+
**Typed catalog** — subscribe via `ctx.hooks.on(eventName, handler)`. TypeScript narrows the payload type per event:
|`content:after:create`| After a document is created |
251
+
|`content:after:update`| After a document is updated |
252
+
|`content:after:delete`| After a document is deleted |
253
+
|`content:after:publish`| After a document is published |
254
+
|`content:read`| When a document is read |
255
+
|`auth:registration:completed`| After user self-registration |
256
+
|`auth:password-reset:requested`| When a password reset is requested |
257
+
|`auth:password-reset:completed`| After a password reset is confirmed |
258
+
|`auth:magic-link:consumed`| After a magic-link is consumed |
259
+
|`auth:otp:verified`| After an OTP code is verified |
260
+
261
+
Deprecated short aliases (`content:save`, `content:publish`, `content:create` etc.) still work through the typed surface but emit a console warning — prefer the canonical `content:after:*` names.
249
262
250
-
That's 25 named events spread across eight categories. Plugins can also define custom hook names — there's no enforced enum at runtime — but sticking to the standard names ensures interoperability.
263
+
**Legacy hook bus** — subscribe via `(ctx.raw as any)?.hooks.register(name, handler, priority)`. Used for hooks that predate the typed catalog: `auth:login`, `auth:logout`, `request:start`. These work but have no TypeScript narrowing.
264
+
265
+
Plugins can also define **custom hook names** — there's no enforced enum at runtime — but using the standard names ensures interoperability with other plugins.
251
266
252
267
For an outbound integration story (Slack, Zapier, custom HTTP destinations), see [SonicJS webhooks](/webhooks). The [hooks reference](/hooks) lists every event with its data shape.
253
268
@@ -267,26 +282,26 @@ Each plugin declares its typed settings schema directly in `definePlugin()`. Def
267
282
268
283
```typescript
269
284
import { definePlugin } from'@sonicjs-cms/core'
270
-
import { z } from'zod'
271
285
272
286
const plugin =definePlugin({
273
287
id: 'otp-login',
274
288
name: 'OTP Login',
275
289
version: '1.0.0',
276
290
277
-
configSchema: z.object({
278
-
codeLength: z.number().default(6),
279
-
codeExpiryMinutes: z.number().default(10),
280
-
maxAttempts: z.number().default(3),
281
-
rateLimitPerHour: z.number().default(5),
282
-
}),
291
+
// configSchema is a Record<string, ConfigSchemaField> — NOT a Zod schema
The admin settings UI reads this schema to auto-generate forms. Values are validated by Zod at write time.
304
+
The admin settings UI reads this schema to auto-generate forms. Field types are `'string' | 'number' | 'boolean' | 'select'` — each with a `label`, optional `default`, and type-specific constraints (`min`/`max` for numbers, `format` for strings, `options` for selects). Values are persisted via `SettingsService` and can be read back in handlers with `new SettingsService(db).getCategorySettings(pluginId)`.
290
305
291
306
### 2. The relational `settings` table (via `SettingsService`)
292
307
@@ -317,7 +332,7 @@ The OAuth providers plugin (`packages/core/src/plugins/core-plugins/oauth-provid
317
332
- Mounts admin routes at `/admin/plugins/oauth` for client-credential management
318
333
- Mounts public auth routes at `/auth/oauth/:provider` and `/auth/oauth/:provider/callback`
319
334
- Stores per-provider client IDs and secrets via its `configSchema` settings (validated by Zod)
320
-
- Fires the `auth:login` hook after a successful OAuth callback so other plugins (like security audit) can react
335
+
- Fires the `auth:login` hook (legacy bus) after a successful OAuth callback so other plugins (like security audit) can react
321
336
322
337
You can dig into the full setup in the [OAuth plugin docs](/plugins/oauth) and the [code examples plugin](/plugins/code-examples) which uses a similar pattern for content management.
323
338
@@ -362,7 +377,7 @@ Every plugin contributes at exactly the moment its registration says it should.
362
377
- SonicJS v3 plugins are created with `definePlugin()` and registered via `registerPlugins()` — no `PluginBuilder`, no `PluginManager` class.
363
378
- Three tiers (core, available, user) share a single registration pipeline and lifecycle.
364
379
- Two lifecycle entry points: `register(app)` (sync, route/middleware mounting) and `onBoot(ctx)` (async, DB-ready setup).
365
-
- Settings are declared as a Zod `configSchema` inside `definePlugin()` — no `plugins.settings` JSON column or `006_plugin_system.sql` migration.
380
+
- Settings are declared as a `configSchema`(`Record<string, ConfigSchemaField>`) inside `definePlugin()` — not Zod. Types are `'string' | 'number' | 'boolean' | 'select'`. No `plugins.settings` JSON column or extra migration.
366
381
- The `HookSystemImpl` is a priority-ordered, scoped, transformative event bus with built-in recursion detection.
367
382
- Route mounting order is part of the contract — plugin routes register before generic `/admin/plugins/:id` to avoid being shadowed.
368
383
- The admin UI reads plugin state from the in-memory registry, not from dedicated plugin database tables.
0 commit comments