@nightowlsdev/storage-supabase 0.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -62
- package/dist/index.cjs +496 -20
- package/dist/index.d.cts +119 -16
- package/dist/index.d.ts +119 -16
- package/dist/index.js +486 -19
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,90 +1,53 @@
|
|
|
1
1
|
# @nightowlsdev/storage-supabase
|
|
2
2
|
|
|
3
3
|
A Supabase/Postgres-backed `StorageAdapter` for `@nightowlsdev/core` — implements the
|
|
4
|
-
`agents` / `runs` / `events` / `messages` seams, a
|
|
5
|
-
bus
|
|
6
|
-
suspend/resume.
|
|
4
|
+
`agents` / `runs` / `events` / `messages` / `scratchpad` seams, a Postgres LISTEN/NOTIFY
|
|
5
|
+
cache-invalidation bus, a cross-instance `ContainerFloor` for serverless mutual exclusion,
|
|
6
|
+
and a Mastra-backed durable suspend/resume store.
|
|
7
7
|
|
|
8
8
|
The server adapter authenticates with the **secret** key (`BYPASSRLS`) and is the real
|
|
9
9
|
authorization boundary — it scopes every query by `tenantId`/`org_id` in code. The SQL
|
|
10
10
|
RLS in the packaged `0001_core` migration is defense-in-depth for the browser /
|
|
11
11
|
Realtime read path only.
|
|
12
12
|
|
|
13
|
-
The tables live in a dedicated **`
|
|
14
|
-
|
|
15
|
-
sets `search_path=nightowls,public`, so the adapter's own queries stay unqualified. The
|
|
13
|
+
The tables live in a dedicated **`nightowls`** schema. The adapter pool sets
|
|
14
|
+
`search_path=nightowls,public`, so the adapter's own queries stay unqualified. The
|
|
16
15
|
schema + tables are created by this package's **inlined migrations** (`MIGRATIONS`),
|
|
17
16
|
which `@nightowlsdev/cli` **installs** into your host's `supabase/migrations/` — you apply them
|
|
18
17
|
with your own tooling (`supabase db push`). Night Owls never runs DDL itself.
|
|
19
18
|
|
|
20
|
-
##
|
|
19
|
+
## Install
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`pg` and `@mastra/pg` rely on prepared statements, which **break on the Transaction
|
|
25
|
-
pooler (port 6543)**. Always pass the **Session/Direct** Postgres URL (port **5432**).
|
|
26
|
-
Both `createSupabaseStorage` and `createMastraPgStore` throw if they detect `:6543` in
|
|
27
|
-
the `dbUrl`.
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
✅ postgresql://…@db.<ref>.supabase.co:5432/postgres (Session/Direct)
|
|
31
|
-
❌ postgresql://…@…pooler.supabase.com:6543/postgres (Transaction — breaks pg)
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @nightowlsdev/storage-supabase
|
|
32
23
|
```
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
| Key | Where it goes | Why |
|
|
37
|
-
|---|---|---|
|
|
38
|
-
| **secret** (`sb_secret_…` / `service_role`) | the server **adapter** (`secretKey`) | `BYPASSRLS`; the real authz boundary. Never ships to the browser. |
|
|
39
|
-
| **publishable** (`sb_publishable_…` / `anon`) | the **browser / Realtime** client | RLS-gated; used by the host app to subscribe to `run:<id>` broadcasts as the end user. |
|
|
40
|
-
|
|
41
|
-
The adapter is server-only. The publishable key is for the host app's browser Realtime
|
|
42
|
-
subscription, which the defense-in-depth RLS policy (`members_receive_run_events`)
|
|
43
|
-
gates by org membership.
|
|
44
|
-
|
|
45
|
-
## Applying the schema: install via `@nightowlsdev/cli`
|
|
46
|
-
|
|
47
|
-
This package ships its migrations **inlined** (`MIGRATIONS`: `{ version, name, sql }[]`,
|
|
48
|
-
fully-qualified `nightowls.*`) and exports the `nightOwlsPlugin` manifest that `@nightowlsdev/cli`
|
|
49
|
-
discovers. You don't apply them from this package — instead the CLI **installs** them into
|
|
50
|
-
your host's classic `supabase/migrations/`, and you apply them with your own tooling:
|
|
25
|
+
Peer dependencies (must be installed by the host):
|
|
51
26
|
|
|
52
27
|
```bash
|
|
53
|
-
|
|
54
|
-
supabase db push # apply them (your tooling — Night Owls never runs DDL)
|
|
28
|
+
pnpm add @nightowlsdev/core @mastra/core @mastra/pg
|
|
55
29
|
```
|
|
56
30
|
|
|
57
|
-
The install writes one `<timestamp>_corale_<version>.sql` per migration and is idempotent
|
|
58
|
-
(already-installed versions are skipped). `owl db install` re-installs after an upgrade.
|
|
59
|
-
|
|
60
|
-
## Mastra durable resume: `disableInit:true` + the `0002_mastra` migration
|
|
61
|
-
|
|
62
|
-
On an RLS-enabled Supabase project you do **not** want `PostgresStore` to run its own
|
|
63
|
-
DDL/init at runtime. `createMastraPgStore` defaults to `disableInit: true` and passes
|
|
64
|
-
`schemaName: 'nightowls'`, so the `nightowls.mastra_*` tables **must pre-exist**. They are
|
|
65
|
-
created by the packaged `0002_mastra` migration (the real `@mastra/pg` DDL via
|
|
66
|
-
`exportSchemas('nightowls')`), including the resume-critical
|
|
67
|
-
`nightowls.mastra_workflow_snapshot`. The installed migrations (above) create them.
|
|
68
|
-
|
|
69
31
|
## Usage
|
|
70
32
|
|
|
71
33
|
```ts
|
|
72
34
|
import {
|
|
73
35
|
createSupabaseStorage,
|
|
74
36
|
createMastraPgStore,
|
|
37
|
+
createMastraVectorStore,
|
|
38
|
+
createPostgresFloor,
|
|
75
39
|
publishAgentVersion,
|
|
76
40
|
} from "@nightowlsdev/storage-supabase";
|
|
77
41
|
import { SwarmEngine, allowListModelProvider } from "@nightowlsdev/core";
|
|
78
42
|
|
|
79
43
|
const storage = createSupabaseStorage({
|
|
80
|
-
url: process.env.SUPABASE_URL!,
|
|
81
|
-
secretKey: process.env.SUPABASE_SECRET_KEY!, //
|
|
82
|
-
dbUrl: process.env.
|
|
83
|
-
// ssl defaults to `true` (verify cert) for remote dbUrl; localhost
|
|
44
|
+
url: process.env.SUPABASE_URL!, // API URL — for supabase-js Realtime
|
|
45
|
+
secretKey: process.env.SUPABASE_SECRET_KEY!, // service_role / sb_secret_… (BYPASSRLS)
|
|
46
|
+
dbUrl: process.env.DATABASE_URL!, // Session/Direct Postgres URL (port 5432)
|
|
47
|
+
// ssl defaults to `true` (verify cert) for remote dbUrl; localhost stays unencrypted.
|
|
84
48
|
});
|
|
85
49
|
|
|
86
50
|
// Seed/publish an agent version (management helper, not part of the core interface).
|
|
87
|
-
// Pass the adapter's typed internal handle `storage.ctx` (no `as any` coupling).
|
|
88
51
|
await publishAgentVersion(storage.ctx, {
|
|
89
52
|
tenantId: orgId,
|
|
90
53
|
slug: "support",
|
|
@@ -93,23 +56,29 @@ await publishAgentVersion(storage.ctx, {
|
|
|
93
56
|
capabilities: [],
|
|
94
57
|
skillNames: [],
|
|
95
58
|
delegateSlugs: [],
|
|
96
|
-
modelId: "openai/gpt-
|
|
59
|
+
modelId: "openai/gpt-4o",
|
|
97
60
|
});
|
|
98
61
|
|
|
99
62
|
// Cross-process durable suspend/resume: inject the Postgres-backed Mastra store.
|
|
63
|
+
// Cross-instance mutual exclusion (serverless): inject the Postgres floor.
|
|
100
64
|
const engine = new SwarmEngine({
|
|
101
65
|
storage,
|
|
102
|
-
model: allowListModelProvider({ allow: ["openai/gpt-
|
|
66
|
+
model: allowListModelProvider({ allow: ["openai/gpt-4o"] }),
|
|
103
67
|
modelFactory: (modelId) => /* your LanguageModelV3 for modelId */ undefined as never,
|
|
104
68
|
cost: { maxSteps: 10, maxCostUsd: 1 },
|
|
105
|
-
mastraStore: createMastraPgStore({ dbUrl: process.env.
|
|
69
|
+
mastraStore: createMastraPgStore({ dbUrl: process.env.DATABASE_URL! }),
|
|
70
|
+
floor: createPostgresFloor(storage.ctx.pool),
|
|
106
71
|
});
|
|
107
72
|
|
|
73
|
+
// Wire cross-process agent-cache invalidation (LISTEN/NOTIFY).
|
|
74
|
+
// Call once, before any run; storage.close() tears it down.
|
|
75
|
+
storage.subscribeInvalidations((key) => { /* key = "tenantId:slug" */ });
|
|
76
|
+
|
|
108
77
|
for await (const e of engine.run({ message: "hi" }, ctx)) {
|
|
109
78
|
// SwarmEvents are persisted to nightowls.events and broadcast on run:<id>
|
|
110
79
|
}
|
|
111
80
|
|
|
112
|
-
await storage.close(); // tears down Realtime channels, then ends the pg pool
|
|
81
|
+
await storage.close(); // tears down the LISTEN connection, Realtime channels, then ends the pg pool
|
|
113
82
|
```
|
|
114
83
|
|
|
115
84
|
Subscribe to the live event stream (server-side, BYPASSRLS):
|
|
@@ -120,6 +89,186 @@ for await (const event of storage.events.subscribe(runId)) {
|
|
|
120
89
|
}
|
|
121
90
|
```
|
|
122
91
|
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
### `createSupabaseStorage(opts: SupabaseStorageOpts): SupabaseStorage`
|
|
95
|
+
|
|
96
|
+
Creates the Supabase `StorageAdapter`. Returns a `SupabaseStorage` which extends
|
|
97
|
+
`StorageAdapter` with a typed internal handle (`ctx`) and a `close()` teardown.
|
|
98
|
+
|
|
99
|
+
**`SupabaseStorageOpts`**
|
|
100
|
+
|
|
101
|
+
| Field | Type | Purpose |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `url` | `string` | Supabase project API URL — for supabase-js Realtime |
|
|
104
|
+
| `secretKey` | `string` | `service_role` / `sb_secret_…` key — BYPASSRLS; never ships to the browser |
|
|
105
|
+
| `dbUrl` | `string` | Session/Direct Postgres URL, **port 5432** (not 6543 — throws if detected) |
|
|
106
|
+
| `max?` | `number` | pg pool size (default 10) |
|
|
107
|
+
| `ssl?` | `boolean \| { rejectUnauthorized: boolean }` | TLS override; defaults to `true` (verify cert) for remote URLs |
|
|
108
|
+
|
|
109
|
+
**`SupabaseStorage`** (returned value)
|
|
110
|
+
|
|
111
|
+
Implements `StorageAdapter` plus:
|
|
112
|
+
|
|
113
|
+
- `ctx: Ctx` — typed internal handle (`{ pool: Pool; sb: SupabaseClient }`); pass to
|
|
114
|
+
management helpers (`publishAgentVersion`, `createPostgresFloor`, etc.) without `as any`.
|
|
115
|
+
- `subscribeInvalidations(onInvalidate: (key: string) => void): () => void` — registers a
|
|
116
|
+
Postgres LISTEN subscription (one per storage, enforced). `onInvalidate` is called with
|
|
117
|
+
`"tenantId:slug"` whenever `publishAgentVersion` NOTIFYs after a commit. Returns an
|
|
118
|
+
unsubscribe function; `close()` also tears it down.
|
|
119
|
+
- `close(): Promise<void>` — releases the LISTEN connection, removes Realtime channels,
|
|
120
|
+
then ends the pg pool.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### `createPostgresFloor(pool: Pool, opts?: PostgresFloorOpts): ContainerFloor`
|
|
125
|
+
|
|
126
|
+
A cross-instance `ContainerFloor` backed by Postgres (FIFO, heartbeat-liveness, stale-lock
|
|
127
|
+
reclaim). Wire it into `SwarmEngine` for serverless mutual exclusion:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
floor: createPostgresFloor(storage.ctx.pool)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**`PostgresFloorOpts`**
|
|
134
|
+
|
|
135
|
+
| Field | Default | Purpose |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `maxHoldMs?` | `180_000` | Stale-lock reclaim window (a held lock older than this is stealable) |
|
|
138
|
+
| `pollMs?` | `250` | Poll interval while queued for the floor |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### `publishAgentVersion(ctx: Ctx, def: PublishAgentDef): Promise<{ version: number }>`
|
|
143
|
+
|
|
144
|
+
Inserts a new agent version, flips the head pointer, audits, and NOTIFYs the R12
|
|
145
|
+
cache-invalidation channel. Enforces the non-bypassable agent-bar (SP6): an `agent`
|
|
146
|
+
principal can never publish. Returns the derived `(max+1)` version number.
|
|
147
|
+
|
|
148
|
+
**`PublishAgentDef`** — `AgentVersionContent & { tenantId: string; actor?: SwarmActor; version?: number }`
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### `rollbackAgentVersion(ctx: Ctx, args): Promise<{ version: number; restoredFrom: number }>`
|
|
153
|
+
|
|
154
|
+
Append-only rollback: republishes a prior version's content as a NEW head (like `git revert`,
|
|
155
|
+
not `reset`). Audited as `action='rollback'`. NOTIFYs R12 cache invalidation. Throws if
|
|
156
|
+
`toVersion` doesn't exist for this tenant/slug.
|
|
157
|
+
|
|
158
|
+
**`args`:** `{ tenantId: string; slug: string; toVersion: number; actor?: SwarmActor }`
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### `listAgentVersions(ctx: Ctx, tenantId: string, slug: string): Promise<AgentVersionInfo[]>`
|
|
163
|
+
|
|
164
|
+
Returns all versions for an agent (oldest → newest), flagging the current head. Use to
|
|
165
|
+
pick a `toVersion` for `rollbackAgentVersion`. Returns `[]` for an unknown agent.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### `makeVersionedRepo(ctx: Ctx): VersionedRepo`
|
|
170
|
+
|
|
171
|
+
The writable agent definition repo (read + publish + rollback + listVersions) backed by
|
|
172
|
+
Postgres. `createSupabaseStorage` wires this as both `storage.agents` and
|
|
173
|
+
`storage.agentsWritable`.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### `createMastraPgStore(opts: { dbUrl: string; disableInit?: boolean }): PostgresStore`
|
|
178
|
+
|
|
179
|
+
A `@mastra/pg` `PostgresStore` scoped to the `nightowls` schema with `disableInit: true`
|
|
180
|
+
(the `nightowls.mastra_*` tables are created by the packaged `0002_mastra` migration — not
|
|
181
|
+
at runtime). Backs durable suspend/resume (`nightowls.mastra_workflow_snapshot`). Throws
|
|
182
|
+
if `dbUrl` is port 6543.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### `createMastraVectorStore(opts: { dbUrl: string }): PgVector`
|
|
187
|
+
|
|
188
|
+
A `@mastra/pg` `PgVector` scoped to the `nightowls` schema — backs semantic recall. The
|
|
189
|
+
`pgvector` extension is enabled by the packaged `0004_memory_vector` migration; `PgVector`
|
|
190
|
+
auto-creates its index table on first use. Throws if `dbUrl` is port 6543.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### `MIGRATIONS: Migration[]`
|
|
195
|
+
|
|
196
|
+
The packaged SQL migration set (`{ version, name, sql }[]`, fully-qualified
|
|
197
|
+
`nightowls.*`), consumed by `nightOwlsPlugin`. Covers 15 migrations: core schema,
|
|
198
|
+
Mastra tables, followups, pgvector memory, thread text IDs, scratchpad, floor locks,
|
|
199
|
+
Realtime presence/broadcast, thread-scoped resource IDs, schema rename, agent version
|
|
200
|
+
immutability, and dropping the SaaS-only `tenant_policies`/`eval_runs` tables (host-owned,
|
|
201
|
+
never used by the engine).
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `nightOwlsPlugin`
|
|
206
|
+
|
|
207
|
+
The plugin manifest that `@nightowlsdev/cli` discovers. Declares:
|
|
208
|
+
|
|
209
|
+
- `migrations` — ejected into the host's `supabase/migrations/` at install
|
|
210
|
+
- `env` — `SUPABASE_URL`, `SUPABASE_SECRET_KEY`, `DATABASE_URL`, `SUPABASE_ANON_KEY`
|
|
211
|
+
merged into `.env.example`
|
|
212
|
+
- `config` — import + wiring snippet inserted at the `// nightowls:storage` marker
|
|
213
|
+
- `commands` — `owl storage-supabase info` (pure, no DB/network)
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Key exported types
|
|
218
|
+
|
|
219
|
+
- `SupabaseStorageOpts` — constructor options
|
|
220
|
+
- `SupabaseStorage` — the returned adapter (`StorageAdapter & { ctx; close; subscribeInvalidations }`)
|
|
221
|
+
- `Ctx` — internal pool + supabase-js client handle (`{ pool: Pool; sb: SupabaseClient }`)
|
|
222
|
+
- `PublishAgentDef` — argument to `publishAgentVersion`
|
|
223
|
+
- `PostgresFloorOpts` — options for `createPostgresFloor`
|
|
224
|
+
- `Migration` — one migration entry (`{ version, name, sql }`)
|
|
225
|
+
- `AgentVersionInfo` — re-exported from `@nightowlsdev/core`
|
|
226
|
+
|
|
227
|
+
## Configuration / Environment
|
|
228
|
+
|
|
229
|
+
The env vars this package reads at runtime (via `SupabaseStorageOpts` / plugin manifest):
|
|
230
|
+
|
|
231
|
+
| Variable | Purpose |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `SUPABASE_URL` | Supabase project API URL (for supabase-js Realtime) |
|
|
234
|
+
| `SUPABASE_SECRET_KEY` | `service_role` / `sb_secret_…` key — BYPASSRLS; server-only |
|
|
235
|
+
| `DATABASE_URL` | Session/Direct Postgres URL — **port 5432** (not 6543) |
|
|
236
|
+
| `SUPABASE_ANON_KEY` | publishable/anon key — browser auth + Realtime only (not read by the adapter itself) |
|
|
237
|
+
|
|
238
|
+
### Use the Session/Direct pooler port (5432) — NEVER the Transaction pooler (6543)
|
|
239
|
+
|
|
240
|
+
`pg` and `@mastra/pg` rely on prepared statements, which **break on the Transaction
|
|
241
|
+
pooler (port 6543)**. Both `createSupabaseStorage` and `createMastraPgStore` and
|
|
242
|
+
`createMastraVectorStore` throw if they detect `:6543` in the URL.
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
✅ postgresql://…@db.<ref>.supabase.co:5432/postgres (Session/Direct)
|
|
246
|
+
❌ postgresql://…@…pooler.supabase.com:6543/postgres (Transaction — breaks pg)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Applying the schema: install via `@nightowlsdev/cli`
|
|
250
|
+
|
|
251
|
+
This package ships its migrations **inlined** and exports the `nightOwlsPlugin` manifest
|
|
252
|
+
that `@nightowlsdev/cli` discovers. Install and apply:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
owl install storage-supabase # installs migrations into supabase/migrations/
|
|
256
|
+
supabase db push # apply them (your tooling — Night Owls never runs DDL)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The install writes one `<timestamp>_nightowls_<version>.sql` per migration and is
|
|
260
|
+
idempotent (already-installed versions are skipped). `owl db install` re-installs after an
|
|
261
|
+
upgrade.
|
|
262
|
+
|
|
263
|
+
## Plugin manifest
|
|
264
|
+
|
|
265
|
+
`nightOwlsPlugin` is the declarative manifest consumed by `@nightowlsdev/cli` (`owl install`
|
|
266
|
+
and `owl storage-supabase info`). It declares the migration set, the env vars to merge into
|
|
267
|
+
`.env.example`, the import + wiring snippet for the `// nightowls:storage` marker, and a
|
|
268
|
+
pure `info` subcommand that prints the installed migrations and required env vars. Night
|
|
269
|
+
Owls never applies DDL itself — all schema changes go through `supabase db push` (or your
|
|
270
|
+
equivalent tooling).
|
|
271
|
+
|
|
123
272
|
## Known limitations
|
|
124
273
|
|
|
125
274
|
- **Multi-tenant read-scoping (C1) deferred.** `events.list`, `runs.loadSnapshot`, and
|
|
@@ -128,11 +277,6 @@ for await (const event of storage.events.subscribe(runId)) {
|
|
|
128
277
|
pass it. For single-tenant v1 the caller (engine/runner) authorizes the `runId` /
|
|
129
278
|
`threadId` before reading. Multi-tenant hardening = widening those core signatures to
|
|
130
279
|
take `tenantId`, tracked for the runner/auth plan (Plan 3).
|
|
131
|
-
- **Cross-instance row-cache bust deferred.** The adapter's per-instance `RowCache`
|
|
132
|
-
(LRU+TTL) for run→org lookups, and the engine's `AgentVersion` cache, are not busted
|
|
133
|
-
across instances. A `postgres_changes` subscription on `nightowls.agents.current_version_id`
|
|
134
|
-
to invalidate caches cluster-wide is deferred to the runner/multi-instance plan; the
|
|
135
|
-
Plan-1 TTL is sufficient until then.
|
|
136
280
|
- **Other v1 simplifications:** `RunStore.setStatus` ignores its optional `patch`
|
|
137
281
|
(status-only persistence); `runner` is hardcoded `'nextjs'` (`NewRun` has no runner
|
|
138
282
|
field); `events.subscribe` shares the adapter's single secret-role `SupabaseClient`
|