Quickstart
Build an osgarden app: declare a schema, deploy it to a gateway, and talk to it with the TypeScript SDK. Nothing else — no servers, no billing code, no secrets.
Hosted deploys are invite-gated during preview. The fastest way to explore is to run the whole platform locally (it's one repo), or to poke at the live demo: chat.osgarden.net. Want to ship on the hosted platform? Open an issue and say hi.
1. Run the platform locally#
You need Bun and a local Postgres.
git clone https://github.com/CarsonJoe/osgarden.git
cd osgarden
bun install
# point the gateway at your Postgres + an Ory project (see packages/gateway/.env.example)
cp packages/gateway/.env.example packages/gateway/.env
bun run db:migrate # ledger tables
bun run dev:gateway # gateway on http://localhost:3000The gateway is the only server there is: it authenticates callers, enforces access, meters usage, and fronts Postgres, storage, and the AI provider.
2. Declare your app's schema#
An app's entire backend is one file, .osgarden.schema.ts — tables, resource types, and
default-deny access rules. This is the demo chat app's real schema, trimmed:
import { defineSchema } from "@osgarden/schema";
export default defineSchema({
tables: {
// Top-level tables are creator-owned: each row lives in its
// author's namespace and can be published into resources.
messages: (table) => {
table.text("body").notNull();
table.timestamps();
table.index(["createdAt"]);
},
},
resources: {
// A "room" anyone can discover and join.
room: (room) => {
room.visibility("discoverable");
room.defaultRole("writer");
// Messages are shared into rooms: authors keep ownership,
// admins can unlink (moderate) without deleting the author's copy.
room.shares("messages", (m) => {
m.onMemberRemove("remove");
m.onOwnerDelete("tombstone");
m.access({
read: "reader",
create: "writer",
update: "creator",
delete: "creator",
unlink: "admin",
});
});
},
},
});Access is default-deny — a verb you don't grant is denied. See Schema & deploy for the full DSL.
3. Deploy it#
bunx osgarden deploy \
--gateway http://localhost:3000 \
--deployment-id myapp \
--token <DEPLOY_TOKEN>The gateway compiles the schema into an isolated Postgres schema and the access metadata it enforces on every request. Safe changes (new tables/columns) apply automatically on redeploy; destructive ones are blocked until you write an explicit migration.
Optionally generate row types for end-to-end type safety:
bunx osgarden typegen # → osgarden-env.d.ts4. Talk to it from your app#
import { createClient } from "@osgarden/sdk";
const osgarden = createClient({
gatewayUrl: "http://localhost:3000",
clientId: "your-oauth-client-id",
});
// Sign in (OAuth + PKCE; tokens stay in httpOnly cookies).
await osgarden.auth.signIn();
// Private data — scoped to the signed-in user, paid by them.
await osgarden.table("notes").insert({ title: "Hello" });
// Shared data — create a room, post into it.
const room = await osgarden.resource.create("room", { name: "General" });
await osgarden.resource(room.id).table("messages").insert({ body: "hi!" });
// Read across every room you're a member of, as one query.
const latest = await osgarden
.resources({ type: "room" })
.table("messages")
.orderBy("createdAt", "desc")
.limit(50);
// AI — no key in your app; the caller pays their own tokens.
const res = await osgarden.ai.chat({
model: "openai/gpt-4o",
messages: [{ role: "user", content: "Hello" }],
});Every call above is authenticated, access-checked, and metered by the gateway. If the
user's balance can't cover an operation, it fails with 402 before any spend.
What you didn't write#
No auth pages, no session handling, no billing integration, no API keys, no row-level security bugs, no servers. The user's dashboard — balance, itemized spend, exports, per-app spend caps — exists already and is the same for every app.
Next steps#
- Database — scopes, ownership models, filters, pagination.
- Resources & membership — invites, roles, moderation.
- TypeScript SDK — the full client surface.
- A complete worked example lives in the repo:
apps/demo-chat.