Skip to content

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 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-typePurpose
RowShape of a row returned by select. All columns present, nullable columns typed as T | null.
InsertShape accepted by insert. Required columns are non-optional; columns with defaults are optional.
UpdateShape 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.

Run the db:types script to pull the latest schema from your Supabase project:

Terminal window
npm run db:types

This executes:

Terminal window
npx supabase gen types typescript --project-id "$PROJECT_REF" > src/lib/types/database.ts

SvelteKit’s app.d.ts declares the global App namespace so that locals and $page.data are fully typed throughout the application:

src/app.d.ts
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;
}
}
}
  • locals.supabase is typed as SupabaseClient<Database>, so every .from('table_name') call autocompletes column names and returns the correct Row type.
  • safeGetSession returns a typed tuple of session and user, avoiding the need to cast or assert after calling it.
  • PageData ensures that $page.data.session and $page.data.user are available in every component without explicit typing.

Because the Supabase client is parameterized with Database, every query is type-checked at compile time.

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;
// } | null

The .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 -- required fields enforced, defaults omitted
await supabase.from('ads').insert({
title: 'My Ad', // required
user_id: user.id, // required
// total_budget, status, etc. have defaults
});
// Update -- all fields optional
await supabase.from('ads').update({
title: 'Updated Title',
}).eq('id', adId);

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.

For reusable types based on specific tables, create them in module-level type files rather than modifying database.ts:

src/lib/modules/honeycomb/rooms/types/index.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.