target-supabase-sdk
v0.3.4
Published
Supabase client initializer and data access APIs
Readme
target-supabase-sdk
Supabase client initializer and data access APIs for Target-based backends.
Install
pnpm add target-supabase-sdkThis package declares @supabase/supabase-js, @supabase/postgrest-js, and lodash-es as peer dependencies — your app must have them available at runtime, but they are not bundled into the SDK itself. This avoids duplicate Supabase client instances and lets you control dependency versions.
pnpm 8+ and npm 7+ usually auto-install missing peer dependencies, so the command above is often enough.
If you see peer dependency warnings or Cannot find module errors at runtime, install them explicitly:
pnpm add target-supabase-sdk @supabase/supabase-js @supabase/postgrest-js lodash-es
@supabase/postgrest-jsis also pulled in transitively by@supabase/supabase-js; it is listed here because the SDK uses its types directly (e.g.PostgrestFilterBuilder).
Environment Variables
Copy .env.example to .env.local and fill in your Supabase project values:
cp .env.example .env.local| Variable | Required | Description |
|----------|----------|-------------|
| SUPABASE_URL | Yes | Main Supabase project URL |
| SUPABASE_ANON_KEY | Yes | Main project anon (public) key |
| SUPABASE_NEED_AUTH_URL | No | Separate auth project URL |
| SUPABASE_NEED_AUTH_ANON_KEY | No | Separate auth project anon key |
Used by scripts/ (pnpm worker:trigger) via .env.local. Browser apps pass the same values to supabase.initialize() (e.g. import.meta.env.VITE_SUPABASE_URL if your bundler uses a VITE_ prefix).
Security
- Only use the anon (public) key in client or script env — it is designed to be public when protected by Row Level Security (RLS).
- Never put the
service_rolekey in env files or frontend code. Use it only in trusted server-side environments. - Do not commit
.envor.env.local. They are gitignored; only.env.example(placeholders) should be tracked. - Auth APIs return session tokens. Store them securely and never log them.
Usage
Browser / bundlers (default)
Chrome extensions, React, Vite — import from the main entry (no Node built-ins):
import { supabase, getTarget, postLinkCreate, postTask } from "target-supabase-sdk";postTask enqueues a task row (Zod payload only). Repo and params validation run at worker prepareTask, not at publish time.
Node.js workers / CLI
Local task registry, repo script loading, TaskNode — use the /node entry:
import { TaskManager, RepoManager, TaskNode, postTaskWithValidation } from "target-supabase-sdk/node";Consumer services (e.g. ../download-service, ../watch-service) run TaskNode / TriggerNode with their own config/task.config.js and tasks/ packages.
Breaking change in 0.2.0:
TaskManager,RepoManager, andpostTaskWithValidationare no longer the only task publish path on/node.postTask(no Repo validation) is on the default browser entry and re-exported from/node.
Browser vs Node (package layout)
Since 0.2.0, the package ships two compiled entries:
| Import | Built file | Runtime |
|--------|------------|---------|
| target-supabase-sdk | dist/browser.js | Chrome extension, React, Vite — no Node built-ins |
| target-supabase-sdk/node | dist/node.js | Workers, CLI, TaskNode — includes browser API + Node-only code |
| target-supabase-sdk/browser | dist/browser.js | Explicit alias of default |
src/browser.ts is the curated browser public surface (includes postTask). src/node.ts re-exports browser and adds Node-only symbols (TaskManager, postTaskWithValidation, RepoManager, …).
Initialize Supabase
import { supabase, getTarget, loginUser } from "target-supabase-sdk";
await supabase.initialize({
supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
supabaseNeedAuthUrl: import.meta.env.VITE_SUPABASE_NEED_AUTH_URL,
supabaseNeedAuthAnonKey: import.meta.env.VITE_SUPABASE_NEED_AUTH_ANON_KEY,
});
const target = await getTarget({ id: "..." });Service catalog
Register services and Api capabilities in Supabase Target rows. L3 startup uses claimServiceRegistrySlot (see l3-service-host) — each process creates a new Service instance and claims a registry slot.
import {
postApi,
postService,
getService,
getApi,
claimServiceRegistrySlot,
ApiMethod,
ServiceLifecycleStatus,
} from "target-supabase-sdk";
// Register an Api capability
await postApi({
name: "Upload Chunk",
value: "storage.post.chunk.0",
tagList: [],
details: {
method: ApiMethod.POST,
path: "/v0/chunks",
endpoint: "http://localhost:3100/v0/chunks",
request: { query: [] },
response: { "200": [] },
manifestVersion: 0,
lifecycle: {
status: ServiceLifecycleStatus.ACTIVE,
activeSince: "2026-01-01",
deprecatedAt: null,
sunsetAt: null,
},
},
});
// L3 startup: new instance + registry slot claim
const session = await claimServiceRegistrySlot({
serviceValue: "storage-service",
createInstance: () =>
postService({
name: "Storage Service",
value: "storage-service",
tagList: [],
details: {
manifestVersion: 0,
apiKeys: ["storage.post.chunk.0"],
dependencies: [],
lifecycle: { /* same shape as Api */ },
},
}).then((r) => {
if (!r.success || r.data == null) throw new Error(r.error?.message ?? "postService failed");
return r.data;
}),
});
// session.release() on graceful shutdown| API | Description |
|-----|-------------|
| postApi / getApi | Create or fetch Api Target rows |
| postService / getService | Create or fetch Service rows (getService({ id }) for a specific instance) |
| claimServiceRegistrySlot | Preflight + register slot for a new instance (createServiceHost wraps this) |
| getTargetSystemRegistry | Read slot layout and which instance ids are ACTIVE |
Types: Service, Api, ApiDetails (method, path, endpoint, request, response), FieldDefinition for inline schemas.
Example consumers: ../storage-service (chunk upload/download), ../watch-service (site scan scheduler), ../download-service (Chrome-backed auto-chrome executor).
TriggerNode (local interval runners)
TriggerNode registers with Supabase (heartbeat, commands) and runs in-process interval runners registered in code (TriggerManager.registerRunner). There is no category=trigger Target type in the SDK.
Register runners in code before start() (registration closes on bootstrap):
import {
TriggerManager,
TriggerNode,
LOG_TOPIC_TRIGGER,
postTask,
} from "target-supabase-sdk/node";
await supabase.initialize({ supabaseUrl, supabaseAnonKey });
TriggerManager.registerRunner({
key: "daily-weather",
intervalMs: 24 * 60 * 60 * 1000,
retryCount: 3,
retryDelayMs: 5_000,
timeoutMs: 120_000,
fn: async (ctx) => {
const { error } = await postTask({ name: "weather", value: "weather", params: { city: "Tokyo" } });
if (error) throw new Error(error.message);
ctx.logger.success("task posted", { topic: LOG_TOPIC_TRIGGER });
},
});
await new TriggerNode({ requireRunners: true }).start();| Concept | Behavior |
|---------|----------|
| Main loop | Fixed 60s (TRIGGER_LOOP_INTERVAL_MS) |
| intervalMs | Per-runner spacing; values < 60s are warned — effective precision is loop-bound |
| Registration | registerRunner before start(); hasRunner / unregisterRunner for lifecycle |
| Retries | 1 + retryCount per tick; optional retryDelayMs between attempts |
| timeoutMs | Optional per-attempt limit; does not cancel in-flight work |
| Overlap | Running fn when due → skip tick (no backlog) |
| Parallel | Due runners in one round → Promise.all |
| Zero runners | Warn by default; requireRunners: true aborts bootstrap |
| Multi-instance | Run one TriggerNode per scheduler — duplicates fire the same runners |
CLI: pnpm worker:trigger (scripts/run-trigger-node.ts — log + postTask examples).
See .cursor/skills/trigger-local-runners/SKILL.md.
Adding new modules (contributors)
When adding a domain, API, or Manager, classify browser vs Node before registering exports. The default entry must stay bundler-safe (Chrome extensions, Webpack, Vite).
Decision flow
- Scan static dependencies — grep the new code and everything it imports for
node:fs,node:crypto,node:path, etc. - Choose entry:
- No
node:*in the chain → add tosrc/browser.ts(and domainindex.ts) - Uses Node built-ins → add only to
src/node.ts(consumers usetarget-supabase-sdk/node) - Same file mixes both → split into separate leaf modules (example:
task.api.tsvstask-post.api.ts)
- No
- Build —
pnpm buildruns Rollup (dual entrybrowser.js+node.js, shared chunks),tsc --emitDeclarationOnlyfor types, thenscripts/verify-browser-entry.mjs, which scansdist/browser.jsand every./chunks/*file it statically imports and fails if any containnode:…imports (Rollup keepsnode:*as external in shared chunks — Webpack MV3 cannot resolve them).
pnpm build
# [verify:browser] OK — dist/browser.js bundle has no node: imports.Node entry (/node) is bundled by the same Rollup build (peers and node:* stay external); there is no separate verify script for it.
Rules of thumb
| Safe on default entry . | Node entry /node only |
|---------------------------|-------------------------|
| *.interface.ts, Supabase RPC *.api.ts | TaskManager, RepoManager, TaskNode, TriggerNode, TriggerManager |
| createTarget, postLinkCreate, task patch* APIs, postTask | postTaskWithValidation, TaskRepoValidation |
| repo.api (remote) | local-task-registry, repo.script-loader |
Do not rely on dynamic import() to hide Node code: if browser.ts statically re-exports a file, bundlers include the whole module. Keep Node-only code in separate files that browser.ts never touches.
Internal imports: within a domain, import leaf paths (from "./task.api") not the domain barrel (from "./index") to avoid cycles.
Detailed conventions: .cursor/skills/browser-node-exports/SKILL.md and .cursor/skills/library-exports/SKILL.md.
Media Links (video / audio / image / image-list)
Convention on Link (same category=link): value === details.loaderKey. Browser-safe draft/mime helpers on the default entry; register / availability on /node only.
import { buildMediaLinkDraft, buildMediaNameFromLocator } from "target-supabase-sdk";
import { registerMediaLink } from "target-supabase-sdk/node";
await registerMediaLink({
value: "video",
name: "my-key",
storageProvider: process.env.LOCAL_STORAGE_PROVIDER!,
locator: "D:\\\\data\\\\clip.mp4",
localStorageProvider: process.env.LOCAL_STORAGE_PROVIDER!,
tagList: ["cv-service", "media-asr"],
});Details: src/shared/media/README.md and .cursor/skills/link-conventions/SKILL.md.
Feed Links (feed.image / feed.audio / feed.video / feed.image-list)
Convention on Link (same category=link): value === details.loaderKey. Platform key name = {platformName}.{feedKey}. System lifecycle tags (.public, .local, .oss) use a leading dot. Draft on the default entry; ingest .public → .local/.oss on /node via registerFeed (locator validation + optimistic lock).
import { buildFeedLinkDraft } from "target-supabase-sdk";
import { registerFeed } from "target-supabase-sdk/node";
const draft = buildFeedLinkDraft({
value: "feed.video",
platformName: "bilibili",
feedKey: "12345",
source: ".public",
locator: "https://example.com/feed/12345",
tagList: ["creator-x"],
});
await registerFeed({
id: feedRowId,
source: ".local",
storageProvider: process.env.LOCAL_STORAGE_PROVIDER!,
locator: "D:\\\\data\\\\bilibili.12345.mp4",
localStorageProvider: process.env.LOCAL_STORAGE_PROVIDER!,
});Details: src/shared/feed/README.md and .cursor/skills/link-conventions/SKILL.md.
PostgREST filter helpers (0.2.15+)
When using supabase.client directly (e.g. ilike search or text[] tag filters):
| Export | Use |
|--------|-----|
| escapeIlikePattern | User input for .ilike() / .or("col.ilike.%…%") — escapes % _ \ and strips , |
| toPostgrestTextArrayLiteral | .overlaps("tagList", …) and .filter("tagList", "not.ov", …) — never .not("tagList", "ov", jsArray) |
Pitfalls and examples: .cursor/skills/target-list-query/SKILL.md.
Development
pnpm install
pnpm typecheck
pnpm typecheck:scripts
pnpm format # Biome: format + lint + organize imports (write)
pnpm format:check # CI gate (read-only)
pnpm verify # typecheck + typecheck:scripts + format:check (same as pre-push)
pnpm build
pnpm devpnpm install sets core.hooksPath to .githooks/. Every git push (including git push --tags) runs pnpm verify first; fix with pnpm format then commit if format fails. Bypass only when intentional: git push --no-verify.
Verify the publishable tarball locally (runs prepack → full build):
pnpm pack:checkCI runs pnpm build then npm pack --dry-run --ignore-scripts so the tarball check does not rebuild.
Release
Publishing is automated via GitHub Actions when a version tag is pushed.
One-time setup
- Create an npm Granular Access Token with Read and Write permission and Bypass 2FA for automation enabled.
- Add it to the GitHub repository as a secret named
NPM_TOKEN:- Repository → Settings → Secrets and variables → Actions → New repository secret
Publish a new version
npm version patch
git push
git push --tagsnpm version patch— bump version (0.1.0→0.1.1), commit, and create git tagv0.1.1. Useminorormajorwhen needed.git push— push the version commit to GitHubgit push --tags— push the tag; triggers.github/workflows/release.ymlto publish to npm
First push on a new machine (set upstream once):
git push -u origin main
git push --tagsPowerShell 5.x does not support &&. Run each command on its own line, or use git push; git push --tags.
The release workflow will:
- Verify the git tag matches
package.jsonversion (e.g. tagv0.1.1↔ version0.1.1) - Build the package
- Publish to npm with provenance
Manual publish (fallback)
pnpm pack:check
npm publish --otp=YOUR_2FA_CODELicense
MIT
