π Database & Storage
Postgres, pgvector, migrations, and Supabase Storage. Add product tables the same way GoShipped already does.
You donβt need to rebuild this part. Add your tables next to chat.
GoShipped uses Supabase Postgres for app data, pgvector for embeddings, and Supabase Storage for uploaded files. The FastAPI app talks to Postgres with SQLAlchemy. You add tables with migrations β not by clicking around in production.
supabase/migrations/ schema (source of truth)
β
pnpm supabase:push hosted
pnpm supabase:start local (applies automatically)
β
apps/api/app/db/ models + sessionWhat is already there
Migrations in supabase/migrations/ β apply all of them, in filename order.
| Area | What you get |
|---|---|
| Users | public.users, filled from Auth (handle_new_user) |
| Chat | conversations, messages, tool_calls |
| RAG | uploaded_files, uploaded_file_chunks + vector search |
| Memory | agent_memory + match_agent_memory |
| Billing | customers, subscriptions, webhook event ids |
| Settings / onboarding | user_settings |
| Usage & safety | usage events, rate-limit counters |
pgvector is enabled in the first migration. Embedding columns are vector(1536) (OpenAI text-embedding-3-small).
supabase/seed.sql is empty on purpose.
How the API uses the database
| Piece | Path |
|---|---|
| URL | DATABASE_URL in apps/api/.env (postgresql+asyncpg://β¦) |
| Session | apps/api/app/db/session.py |
| Models | apps/api/app/db/models.py |
The API uses the database role in DATABASE_URL. That path is not PostgREST. RLS still matters for anything that hits Supabase from the browser; user-owned tables use auth.uid() = user_id. Billing, usage, and similar tables often allow SELECT and keep writes on the API.
Apply migrations
Use the Supabase CLI already pinned in the repo. Do not invent a second migration path.
| Where | Command |
|---|---|
| Local stack | pnpm supabase:start (applies automatically) |
| Reset local data | pnpm supabase:reset |
| New file | pnpm supabase:new -- add_widgets |
| Hosted / linked project | pnpm supabase:push |
| Setup / deploy wizards | supabase db push after they know the DB URL |
Do not run migrations inside the Vercel build. Push with the CLI (or paste SQL in the dashboard, still in filename order) before the API needs the new columns. See database migrations if the CLI workflow is new.
Storage bucket is not a migration
File uploads use a private bucket named chat-files (FILE_STORAGE_BUCKET). Create it in the Supabase Storage dashboard (or API). Setup/doctor expect it when features.files is on. See RAG & Files.
Add a product table
Same pattern as conversations and files:
pnpm supabase:new -- add_widgets- Table + indexes +
ENABLE ROW LEVEL SECURITY+ policies - Mirror the columns in
apps/api/app/db/models.py - Service + route under
apps/api/app/ - Local: reset or push. Hosted:
pnpm supabase:push
CREATE TABLE IF NOT EXISTS public.widgets (
id UUID PRIMARY KEY DEFAULT extensions.uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_widgets_user_id ON public.widgets(user_id);
ALTER TABLE public.widgets ENABLE ROW LEVEL SECURITY;
CREATE POLICY widgets_select_own ON public.widgets
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY widgets_insert_own ON public.widgets
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY widgets_update_own ON public.widgets
FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY widgets_delete_own ON public.widgets
FOR DELETE USING (auth.uid() = user_id);Keep API handlers scoped to CurrentUser.id. Do not expose a generic table to the browser and hope RLS is enough if you have not tested the policies.
Storage
| Fact | Detail |
|---|---|
| Bucket | chat-files (private) |
| Who uploads | FastAPI with the service role (apps/api/app/services/storage_service.py) |
| Object path | users/{user_id}/conversations/{conversation_id}/{file_id}/{filename} |
| Needs | SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, features.files=true |
You do not configure bucket RLS for that server path. Create extra buckets the same way if your product stores other files β still prefer the API as the writer. Official reference: Supabase Storage.
Local vs hosted
Hosted (typical): setup links the project and pushes migrations. No Docker. Create a project in the dashboard if you need one.
Local: pnpm supabase:start needs Docker. Auth mail goes to Mailpit. See local development for the CLI stack itself.
Either way, supabase/ in the repo is the schema. The dashboard is for Auth URLs, SMTP, and creating Storage buckets β not a second source of truth for tables.