Type-safe reactive queries for Supabase with real-time subscriptions and optimistic updates.
Create a schema file (e.g., lib/db/schema.ts) using the provided column types. This schema will be used to generate your database tables and provide end-to-end type safety for your queries.
import {
enumType,
integer,
pgFunction,
table,
text,
timestamp,
uuid,
varchar,
} from "supabase-schema";
// 1. Define Functions
export const updateTimestamp = pgFunction("update_timestamp", {
returns: "trigger",
language: "plpgsql",
definition: `
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
`,
});
// 2. Define Tables with Policies, Indexes, and Triggers
export const goals = table("goals", {
id: uuid({ primaryKey: true }),
title: text({ notNull: true }),
user_id: uuid({ notNull: true }),
created_at: timestamp({ notNull: true, defaultValue: "now()" }),
updated_at: timestamp({ notNull: true, defaultValue: "now()" }),
})
.enableRLS()
// RLS Policies
.policy("Users can only see their own goals", {
for: "select",
to: ["authenticated"],
using: "auth.uid() = user_id"
})
// Composite Indexes
.index(["user_id", "created_at"])
// Triggers
.trigger("handle_updated_at", {
when: "BEFORE",
events: ["UPDATE"],
call: updateTimestamp
});
export const todos = table("todos", {
id: uuid({ primaryKey: true }),
goal_id: uuid({ notNull: true }).references(() => goals.id),
title: varchar({ notNull: true }),
completed: integer({ notNull: true, defaultValue: 0 }),
})
.enableRLS()
.policy("Allow all on todos", { for: "all", to: ["authenticated"], using: "true" });
export const schema = {
goals,
todos,
updateTimestamp,
} as const;
export type AppSchema = typeof schema;Enable RLS and define granular policies directly in your TypeScript code. Support for using and withCheck clauses allows for complex security logic, including referencing pgFunction definitions.
The pgFunction helper allows you to define PostgreSQL functions directly in your schema. These can then be referenced in table triggers using the .trigger() method, supporting BEFORE, AFTER, and INSTEAD OF timings across all standard DML events (INSERT, UPDATE, DELETE, TRUNCATE).
Beyond single-column .index() calls on individual columns, you can define composite indexes and unique constraints on multiple columns using the table-level .index(["col1", "col2"]) and .unique(["col1", "col2"]) methods.
Set the following environment variables in your .env file:
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
DATABASE_URL=your-postgres-connection-stringInitialize the SupabaseProvider in your root layout or a dedicated providers component.
// components/providers.tsx
"use client"
import { SupabaseProvider } from "supabase-react"
import { schema } from "@/lib/db/schema"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SupabaseProvider
url={process.env.NEXT_PUBLIC_SUPABASE_URL!}
publishableKey={process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!}
schema={schema}
>
{children}
</SupabaseProvider>
)
}Fetch data with automatic real-time updates and relation handling.
const { data, isLoading } = useQuery<AppSchema, "goals">("goals", {
include: {
todos: {
where: { completed: 0 }
}
}
});Perform type-safe mutations with optional optimistic updates.
const { insert } = useMutation<AppSchema, "goals">("goals", {
optimisticUpdate: true
});
const handleAdd = () => {
insert({ title: "New Goal" });
};Manage authentication state easily.
const { user, signInAnonymously, signOut } = useAuth();When you update your schema.ts file, push the changes to your Supabase database:
npx supabase-schema push ./lib/db/schema.tsIf you've made changes directly via the Supabase dashboard and want to update your local schema:
npx supabase-schema pullsupabase-react: React hooks and provider.supabase-schema: Schema definition and CLI tools.