Database Types
Honeycomb uses auto-generated TypeScript types from the Supabase schema. These types flow through the entire application — from the Supabase client to server locals to page data — ensuring end-to-end type safety.
The Database interface
Section titled “The Database interface”The file src/lib/types/database.ts is generated, not hand-written. It exports a Database type that describes every table, view, function, and enum in the public schema.
// src/lib/types/database.ts (excerpt)export type Json = | string | number | boolean | null | { [key: string]: Json | undefined } | Json[];
export type Database = { public: { Tables: { account_deletion_feedback: { Row: { created_at: string; feedback: string | null; id: string; reason: string | null; user_id: string | null; }; Insert: { /* fields with optional defaults */ }; Update: { /* all fields optional */ }; Relationships: [ /* foreign key metadata */ ]; }; ads: { Row: { approval: Database['public']['Enums']['approval_status']; title: string; total_budget: number; // ... }; // ... }; // ... every other table }; Enums: { /* all custom enums */ }; Functions: { /* RPC functions */ }; };};Each table has three sub-types:
| Sub-type | Purpose |
|---|---|
Row | Shape of a row returned by select. All columns present, nullable columns typed as T | null. |
Insert | Shape accepted by insert. Required columns are non-optional; columns with defaults are optional. |
Update | Shape accepted by update. Every column is optional (partial update). |
Enum columns reference Database['public']['Enums'][...] so they stay in sync with the Postgres enum definition.
Regenerating types
Section titled “Regenerating types”Run the db:types script to pull the latest schema from your Supabase project:
npm run db:typesThis executes:
npx supabase gen types typescript --project-id "$PROJECT_REF" > src/lib/types/database.tsApp namespace augmentation
Section titled “App namespace augmentation”SvelteKit’s app.d.ts declares the global App namespace so that locals and $page.data are fully typed throughout the application:
import type { SupabaseClient, Session } from '@supabase/supabase-js';import type { Database } from '$lib/types/database';
declare global { namespace App { interface Locals { supabase: SupabaseClient<Database>; safeGetSession(): Promise<{ session: Session | null; user: Session['user'] | null; }>; } interface PageData { session: Session | null; user?: Session['user'] | null; } }}What this enables
Section titled “What this enables”locals.supabaseis typed asSupabaseClient<Database>, so every.from('table_name')call autocompletes column names and returns the correctRowtype.safeGetSessionreturns a typed tuple ofsessionanduser, avoiding the need to cast or assert after calling it.PageDataensures that$page.data.sessionand$page.data.userare available in every component without explicit typing.
Type-safe queries
Section titled “Type-safe queries”Because the Supabase client is parameterized with Database, every query is type-checked at compile time.
Select with column narrowing
Section titled “Select with column narrowing”const { data: profile } = await locals.supabase .from('profiles') .select('id, username, avatar_url, verified, role, type') .eq('id', user.id) .single();
// typeof profile:// {// id: string;// username: string;// avatar_url: string | null;// verified: boolean;// role: string;// type: string;// } | nullThe .select() string is parsed at the type level — only the listed columns appear on the result type.
const { data: installedApps } = await locals.supabase .from('user_apps') .select('app_id, is_active, apps(slug, name, icon)') .eq('user_id', user.id) .eq('is_active', true);The apps(slug, name, icon) syntax follows the foreign key relationship. The result type nests the joined columns under an apps property. In some cases you may need to cast the joined result when TypeScript cannot infer the shape:
const app = ua.apps as unknown as { slug: string; name: string; icon: string | null };Insert and update
Section titled “Insert and update”// Insert -- required fields enforced, defaults omittedawait supabase.from('ads').insert({ title: 'My Ad', // required user_id: user.id, // required // total_budget, status, etc. have defaults});
// Update -- all fields optionalawait supabase.from('ads').update({ title: 'Updated Title',}).eq('id', adId);Real-time payload typing
Section titled “Real-time payload typing”When handling postgres_changes payloads, cast payload.new to the correct Row type:
const newMsg = payload.new as Database['public']['Tables']['messages']['Row'];This gives you full autocomplete and null-safety on the incoming row.
Deriving helper types
Section titled “Deriving helper types”For reusable types based on specific tables, create them in module-level type files rather than modifying database.ts:
import type { Database } from '$lib/types/database';
export type Room = Database['public']['Tables']['rooms']['Row'];export type RoomInsert = Database['public']['Tables']['rooms']['Insert'];export type RoomMessage = Database['public']['Tables']['room_messages']['Row'];This pattern keeps module types in sync with the database schema while remaining readable and locally scoped.