The supabase-js Client
The idea in one sentence
Section titled “The idea in one sentence”createClient builds the one object your app uses to talk to Postgres, Auth, Storage, and Realtime, and it should always be constructed with the public anon key — never the service_role key — because the schema’s row level security policies, not the secrecy of this key, are what actually protect your data.
Initializing the client
Section titled “Initializing the client”Every supabase-js app starts the same way: import createClient, hand it your project URL and a key, and you get back an object with .from(), .auth, .storage, and .channel() on it.
import { createClient } from '@supabase/supabase-js';
// Safe to ship in browser bundles: this is the anon key, not the service_role keyconst supabase = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);This is worth repeating clearly, because it is easy to get backwards: the client in browser or mobile code must always be initialized with the anon key. The anon key is designed to be public — it ships inside your JavaScript bundle where anyone can read it, and that is fine. What actually stops a stranger from reading or writing rows they should not touch is row level security (RLS) on your tables, not any secrecy around this key. The service_role key is a completely different thing: it bypasses RLS entirely, which makes it extremely powerful and exactly why it must never appear in client-side code — it belongs only in trusted server environments (an Edge Function, a backend process) where it never reaches a browser.
Generated types turn .from() into a typed API
Section titled “Generated types turn .from() into a typed API”The Supabase CLI can read your live schema and emit a matching TypeScript Database type:
supabase gen types typescript --project-id abcdefghijklmnopqrst > database.types.tsThat command inspects every table, column, and constraint in your project and generates a type with a Row, Insert, and Update shape per table — Database['public']['Tables']['movies']['Row'] is exactly the columns a select returns, Insert is what a valid new row looks like, and Update is what a partial patch looks like. Passing this type as a generic to createClient wires the whole query builder up to it:
import { createClient } from '@supabase/supabase-js';import type { Database } from './database.types';
const supabase = createClient<Database>( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);
// Autocomplete on table names, column names, and the shape of dataconst { data, error } = await supabase .from('movies') .select('id, title, year');From this point on, misspelling a column name, selecting a table that does not exist, or building an insert payload that is missing a required field all become compile-time TypeScript errors instead of runtime surprises. Whenever your schema changes, re-run the same CLI command to regenerate database.types.ts and the whole app’s types stay in sync with the database.
One client, created once
Section titled “One client, created once”A supabase-js client is typically created a single time and reused everywhere in the app — a singleton — rather than re-created on every request or every component render.
// lib/supabase.ts — created once, imported wherever it is neededimport { createClient } from '@supabase/supabase-js';import type { Database } from './database.types';
export const supabase = createClient<Database>( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);Two practical reasons drive this. First, the client holds session state in memory (and in local storage) — recreating it discards or duplicates that state and can cause auth listeners to fire inconsistently. Second, the client manages its own network resources, including the WebSocket connection Realtime uses — a fresh client per component means redundant connections instead of one shared, reused connection. Importing the same instance everywhere avoids both problems.
flowchart LR
schema[("Your Postgres schema")] --> gen["supabase gen types typescript"]
gen --> types["Generated Database type (database.types.ts)"]
types --> client["createClient<Database>()"]
client --> query["Typed .from() queries with autocomplete"]