Project Setup
Getting Started shows the app side — add the library, create a client, make a call. This page covers the other half: standing up the Supabase backend from nothing, so the URL and anon key that page asks for actually exist.
By the end you’ll have a Supabase project (hosted or local), a schema with proper row-level security, and a Kotlin client talking to it.
Two ways to run a backend: a hosted project (fastest — Supabase runs it for you) or a local stack via the CLI (free, offline, great for development). You can do both and deploy your local schema to the hosted one when ready.
1. Create your project
The quickest start — no tools to install:
Create the project
Go to database.new (or the dashboard), click New project, pick an organization, name it, choose a region close to your users, and set a strong database password (save it — you’ll need it for migrations).
Wait for it to provision
Takes a minute or two. When it’s ready, the project is live with a Postgres database, Auth, Storage and Realtime already running.
2. Get your URL and keys
These are the two values Getting Started needs for
Supabase.create(...).
In the dashboard: Project Settings → API.
- Project URL →
https://<ref>.supabase.co anonpublickey → your clientapiKeyservice_rolekey → server-side only (see the warning below)
Ship the anon key in your app — never the service_role key. The anon key is
safe to publish because Row Level Security decides what each user can access
(next step). The service-role key bypasses RLS and belongs only on a trusted
server (supabase-auth-admin). Never commit it to source.
3. Define your schema with migrations
Don’t click tables together by hand — describe them as migrations (versioned SQL files) so your schema is reproducible and reviewable. Create one:
supabase migration new create_todosThat makes a timestamped file under supabase/migrations/. Fill it in — a table
plus the RLS policies that make the anon key safe:
create table todos (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users (id) default auth.uid(),
title text not null,
done boolean not null default false,
created_at timestamptz not null default now()
);
-- Lock the table down, then grant each user access to only their own rows.
alter table todos enable row level security;
create policy "users read their todos" on todos for select
to authenticated using (auth.uid() = user_id);
create policy "users insert their todos" on todos for insert
to authenticated with check (auth.uid() = user_id);
create policy "users update their todos" on todos for update
to authenticated using (auth.uid() = user_id);
create policy "users delete their todos" on todos for delete
to authenticated using (auth.uid() = user_id);RLS is your security model. With it on, the public anon key can’t read or write anything a policy doesn’t explicitly allow — that’s exactly why the anon key is safe to ship. A table with RLS enabled and no policies denies everything.
Apply it:
supabase db reset # re-runs every migration + seed.sql on the local databaseUse supabase migration up to apply only new migrations without wiping data.
4. Deploy local → hosted
Developed locally and ready to go live? Link your local project to the hosted one, then push the schema:
supabase login # opens a browser to authenticate
supabase link --project-ref <your-ref> # the <ref> from your project URL
supabase db push # applies your migrations to the hosted DBNow the same schema runs in both places, and future changes are just
supabase migration new … → supabase db push.
5. Connect from your Kotlin app
Add the library and create a client with the URL + anon key from step 2 (full walkthrough in Getting Started):
val client = Supabase.create(
projectUrl = "https://your-project.supabase.co", // or http://127.0.0.1:54321 locally
apiKey = "your-anon-key",
) {
logging = true
}
val database = createDatabaseClient(client)Then describe your rows as a @Serializable class that mirrors the table — there’s
no code generation step; the typed model is the contract:
@Serializable
data class Todo(
val id: String,
val title: String,
val done: Boolean,
)
database.selectTyped<Todo>(table = "todos")
.onSuccess { todos -> println("Loaded ${todos.size}") }
.onFailure { error -> println("Error: ${error.message}") }Because RLS scopes rows to the signed-in user, sign a user in first (Authentication) — otherwise an authenticated-only table returns nothing.
Recap
Backend exists
A hosted project, or a local stack via supabase start.
Schema is versioned
Tables + RLS policies live in supabase/migrations/, applied with
supabase db reset (local) or supabase db push (hosted).
Keys in hand
Project URL + anon key (from the dashboard or supabase status); service-role
key kept off the client.
App connected
Supabase.create(url, anonKey) → feature clients → typed @Serializable calls.
From here, continue with Getting Started for the client rhythm, or jump to Authentication, Database and Realtime.