forgeyn-sdk
v0.2.1
Published
Forgeyn Base SDK — universal data-plane client + central auth, server/admin management client, and React hooks. Zero runtime dependencies.
Readme
forgeyn-sdk
The official Forgeyn Base SDK. Zero runtime dependencies — fetch only. Works in browsers, Node 18+, Bun, Deno and edge runtimes.
npm install forgeyn-sdkVersion 0.2 ships three entry points:
| Entry point | Import | For |
| --- | --- | --- |
| Universal client | forgeyn-sdk (or forgeyn-sdk/client) | Browsers, mobile, edge — data plane via the public gateway + end-user auth via central GoTrue |
| Server / admin client | forgeyn-sdk/node | Servers & agents — project provisioning, SQL, tables/rows, gateway apps, app end-users (X-Api-Key) |
| React hooks | forgeyn-sdk/react | React 18/19 apps — ForgeynProvider, useForgeynClient, useAuth, useTable |
// browser / app code
import { createClient } from "forgeyn-sdk";
// server code
import { createAdminClient } from "forgeyn-sdk/node";
// react
import { ForgeynProvider, useAuth, useTable } from "forgeyn-sdk/react";1. Universal client — forgeyn-sdk / forgeyn-sdk/client
import { createClient } from "forgeyn-sdk";
const fb = createClient({
appId: "app_xxxx", // from the /v1 credentials block (credentials.app_id) or console → API keys
clientKey: "pk_ey…", // publishable — safe in the browser
gatewayUrl: "https://pupagsqvhhshecxmbgay.supabase.co/functions/v1/gateway", // optional, this is the default
apiUrl: "https://api.db.forgeyn.com", // optional, this is the default
});Data plane — fb.from(table) (edge gateway, x-app-id + pk_ client key)
Structured JSON actions only — the gateway never sees raw SQL, every operation runs with bind parameters on your project's private Postgres.
await fb.from("todos").insert({ title: "Ship 0.2", done: false });
const { rows } = await fb.from("todos").select({
columns: ["id", "title", "done"],
where: { done: false }, // equality shorthand; operators: { eq, ne, gt, gte, lt, lte, like, ilike, in }
orderBy: "-created_at", // string shorthand; or [{ column: "created_at", direction: "desc" }]
limit: 20,
offset: 0,
});
await fb.from("todos").update({ where: { id }, values: { done: true } });
await fb.from("todos").delete({ where: { id } }); // alias: .remove(...)
const open = await fb.from("todos").count({ where: { done: false } });
// gateway reachability probe → { ok, service: "baas-gateway", configured }
await fb.health();Rows on tables with a user_id column are forced to the Bearer token's user by the gateway. Attach a gateway tenant token with fb.setToken(token); calls without a token run as the anonymous (pk_) identity — public tables work out of the box.
End-user auth — fb.auth (central GoTrue at {apiUrl}/auth/v1)
Tier-2 end-user auth is the platform's central GoTrue service, exposed Supabase-style on the API origin.
const { user, session } = await fb.auth.signUp("[email protected]", "password123");
// session: { access_token, refresh_token, token_type, expires_in, user } — null when email confirmation is required
// (production has mailer_autoconfirm on, so sign-up returns a session immediately)
const res = await fb.auth.signIn("[email protected]", "password123"); // → { user, session }
const me = await fb.auth.getUser(); // GET /auth/v1/user with the session's Bearer token
await fb.auth.signOut(); // POST /auth/v1/logout + local session cleared
fb.auth.getSession(); // stored AuthSession or null
fb.auth.getToken(); // current access_token or null
fb.auth.setSession(s); // restore a session (e.g. from your own storage)Session persistence (opt-in)
Sessions live in memory by default. To survive reloads, opt in explicitly:
const fb = createClient({
appId: "app_xxxx",
clientKey: "pk_…",
persistSession: true, // writes localStorage["forgeyn:session:<appId>"] and restores on load
});Honest security note: raw tokens in localStorage are readable by any XSS payload. For production apps, back persistence with YOUR server (e.g. an httpOnly cookie via a /session endpoint) and call fb.auth.setSession(...) on boot. persistSession is a convenience for prototypes.
2. Server / admin client — forgeyn-sdk/node
Management-plane access with a Forgeyn API key (X-Api-Key: fbk_live_…). Account-wide or project-bound; scopes like projects:read, projects:write, sql:run, keys:write. Bearer tokens are better-auth console sessions — this client never sends them. Server-side only.
import { createAdminClient } from "forgeyn-sdk/node";
const admin = createAdminClient({
apiKey: process.env.FORGEYN_API_KEY!, // fbk_live_…
apiUrl: "https://api.db.forgeyn.com", // optional, this is the default
});Provisioning flow (the Studio /v1 contract)
const created = await admin.projects.create({
owner_external_id: "user_42", // YOUR canonical user id → shadow platform user
name: "my-app-db",
idempotency_key: "user_42:my-app-db", // retries are safe: same key → same project (200 + full record)
owner_email: "[email protected]", // optional — unifies with an existing platform account
owner_name: "Ada Lovelace", // optional
region: "aws-us-east-1", // optional (GET /api/meta/regions)
});
// → 202 { project_id, status: "provisioning", poll_url }
const project = await admin.projects.pollUntilReady(created.project_id, {
timeoutMs: 120_000, // default; Neon provisioning usually takes 5–15s
intervalMs: 2_000, // default
});
// resolves the FINAL state: "ready" (with credentials) or "failed" (with error).The credentials block
project.credentials on a ready project (and on idempotent replay):
| Field | Meaning | Where it may live |
| --- | --- | --- |
| api_url | Forgeyn API origin — management plane + central GoTrue ({api_url}/auth/v1) | any trusted runtime |
| gateway_url | Public edge-gateway URL — the browser data plane | browsers included |
| app_id | Gateway app id (app_…) — sent as x-app-id | browsers included |
| pk_key | Publishable client key (pk_…) — sent as x-client-key | browsers safe (signed ES256 JWT) |
| sk_key | Gateway secret admin key (sk_…) — bypasses per-user row scoping | SERVER-ONLY. Never in a browser, never in client bundles, never in NEXT_PUBLIC_* |
| fbk_key | Project-bound Forgeyn API key (fbk_live_…, scopes projects:read + sql:run) — MCP/CLI/agent surface | server-side |
Note:
gateway_urlis populated from the API deployment'sGATEWAY_PUBLIC_URLenv. If a deployment hasn't set it, the field isnull— passgatewayUrlexplicitly tocreateClient(the SDK defaults to the production gateway).
Which key drives which surface (verified live): management-plane endpoints (sql, tables, rows, auth/users) authorize the project's owner. Projects provisioned through /v1/projects are owned by the shadow user — so drive their schema/rows with the bundled project-bound fbk_key (scopes projects:read + sql:run), and keep your service key for provisioning, status polling and lifecycle (delete). The apps() keys surface resolves the owner too, but for /v1 projects the bundle already carries app_id/pk_key/sk_key, so nothing extra needs minting.
Wire them into the universal client:
const fb = createClient({
appId: project.credentials.app_id,
clientKey: project.credentials.pk_key,
gatewayUrl: project.credentials.gateway_url ?? undefined,
apiUrl: project.credentials.api_url,
});Everything else on the admin client
await admin.health(); // → { ok, service: "forgeyn-api", version, db }
const mine = await admin.projects.list(); // management-plane list (this key's projects)
const p = await admin.projects.get(id); // GET /v1/projects/:id (contract shape)
const { status } = await admin.projects.remove(id); // DELETE → { status: "deleting" } (async teardown)
// SQL (scope: sql:run) — rows capped at 500 per query, 15s statement timeout
const r = await admin.sql(projectId, "select 1 as ok"); // → { columns, rows, rowCount, durationMs }
await admin.sql(projectId, "select * from t", { readonly: true }); // READ ONLY transaction
// Schema + rows
const tables = await admin.tables(projectId); // [{ name, columns, rowCount, sizeBytes }]
const page = await admin.rows(projectId, "todos").list({ limit: 50, offset: 0 });
const row = await admin.rows(projectId, "todos").insert({ title: "hi", done: false });
const upd = await admin.rows(projectId, "todos").update({ id: row.id }, { done: true }); // pk → values
const gone = await admin.rows(projectId, "todos").delete({ id: row.id }); // → deleted count
// Gateway app credentials (dual-key pk_/sk_ pairs) — GET/POST/DELETE /api/projects/:id/keys
const apps = await admin.apps(projectId).list(); // [{ appId, name, status, createdAt, revokedAt }]
const app = await admin.apps(projectId).create({ name: "web" }); // { appId, clientKey, secretKey } — secret shown once
await admin.apps(projectId).revoke(app.appId);
// App end-users (POST /api/projects/:id/auth/users)
const { user } = await admin.appUsers.create(projectId, {
email: "[email protected]",
password: "correct-horse-battery", // optional
email_confirm: true, // optional
user_metadata: { plan: "free" }, // optional
app_metadata: { role: "member" }, // optional
});
// 201 → { user: { id, email, … } }; duplicate email → 409.
// NOTE: this endpoint ships independently of the SDK — while a deployment
// doesn't have it yet it answers 404; treat that as "not available here".3. React — forgeyn-sdk/react
Peer dependency: react ^18 || ^19. No other dependencies, no JSX required (the provider is createElement-based).
import { createClient } from "forgeyn-sdk";
import { ForgeynProvider, useAuth, useForgeynClient, useTable } from "forgeyn-sdk/react";
const fb = createClient({ appId: "app_xxxx", clientKey: "pk_…", persistSession: true });
export default function Root({ children }) {
return <ForgeynProvider client={fb}>{children}</ForgeynProvider>;
}
function Todos() {
const { user, signIn, signOut, loading } = useAuth();
const todos = useTable("todos", { orderBy: "-created_at", limit: 50 });
const client = useForgeynClient();
return (
<>
<p>{user ? `Hi ${user.email}` : "Anonymous"}</p>
<button onClick={() => signIn("[email protected]", "password123")} disabled={loading}>Sign in</button>
<button onClick={signOut}>Sign out</button>
{todos.loading && <p>Loading…</p>}
<ul>{todos.rows.map((r) => <li key={String(r.id)}>{String(r.title)}</li>)}</ul>
<button onClick={() => todos.refresh()}>Refresh</button>
<button onClick={() => client.from("todos").insert({ title: "new", done: false }).then(todos.refresh)}>Add</button>
</>
);
}useAuth()→{ user, loading, error, signUp, signIn, signOut, refreshUser }useTable(table, opts)→{ rows, loading, error, refresh }useForgeynClient()→ the client from the provider (throws without a provider)
Errors
Every non-2xx response throws ForgeynError ({ name, code, status, details, message }) — the class is exported from every entry point (forgeyn-sdk, forgeyn-sdk/client, forgeyn-sdk/node), and instanceof works across entries. Both platform envelopes are parsed: the API envelope { error: { code, message, details } } and the GoTrue envelope { code, error_code, msg }. Network failures throw code: "network_error", status: 0.
Migrating from 0.1.x
createClient({ url, appId, clientKey })→createClient({ appId, clientKey, gatewayUrl })(urlrenamed).fb.db.select/insert/update/remove/count(table, …)→fb.from(table).select/insert/update/delete/count(…).fb.authnow targets central GoTrue at{apiUrl}/auth/v1(Supabase-shaped{ user, session },signUp/signIn/signOut/getUser). The 0.1 gateway auth (/auth/signupper-project accounts) is replaced per the platform auth contract; attach gateway tenant tokens withfb.setToken(token).- The
persisthook is replaced by thepersistSessionboolean flag. createAdminClientmoved toforgeyn-sdk/nodeand is now a management-plane client (X-Api-Key: provisioning, SQL, tables/rows, apps, app users). The 0.1 sk_-based data-plane admin client is available by passingadminKeytocreateClient(server runtimes only — the constructor refusesadminKeyin browsers).
License
MIT
