Skip to content

Local Database & Storage ​

This tutorial covers the database layer that powers LenserFight when running in Supabase mode. You will learn how to manage PostgreSQL, write migrations, work with storage, and understand row-level security.

Prerequisites ​

  • Local Installation completed with Supabase mode
  • Docker Desktop running
  • Supabase CLI installed

PostgreSQL setup ​

LenserFight uses Supabase, which runs PostgreSQL 15+ with these extensions:

ExtensionPurpose
pgcryptoUUID generation
pg_cronScheduled jobs
pgjwtJWT verification
pg_stat_statementsQuery performance
postgisGeolocation (optional)

Starting the database ​

bash
pnpm supabase start

The database runs on port 54322 by default. Direct connection:

bash
psql postgresql://postgres:postgres@127.0.0.1:54322/postgres

Database Studio ​

Supabase provides a web UI for database management:

http://127.0.0.1:54323

Use Studio to browse tables, run SQL queries, and inspect RLS policies.


Schema architecture ​

LenserFight organizes tables into PostgreSQL schemas:

SchemaPurposeKey tables
publicCore entitieslensers, lenses, workflows, workflow_nodes, workflow_edges
agentsAI agent layerworkspace_settings, evaluation_results, runs
battlesBattle enginebattles, submissions, votes, rankings
executionTrust & executionrunner_device_bindings, attestation_verifications
devicesDevice managementregistered_devices
automationScheduling & triggerscron_jobs, automation_rules
reputationXP & scoringxp_events, reputation_scores
auditAudit trailaudit_events, hash_chain
authzAuthorizationdevice_approval_requests, developer_tokens

Migrations ​

Creating a new migration ​

bash
pnpm supabase migration new <descriptive_name>

This creates a timestamped file in supabase/migrations/:

supabase/migrations/20270515100000_descriptive_name.sql

Writing migration SQL ​

sql
-- supabase/migrations/20270515100000_add_agent_memory.sql

-- Create table
CREATE TABLE IF NOT EXISTS agents.memory_entries (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  lenser_id   uuid NOT NULL REFERENCES public.lensers(id) ON DELETE CASCADE,
  content     text NOT NULL,
  embedding   vector(1536),
  created_at  timestamptz NOT NULL DEFAULT now(),
  updated_at  timestamptz NOT NULL DEFAULT now()
);

-- Create index
CREATE INDEX idx_memory_entries_lenser_id ON agents.memory_entries(lenser_id);

-- Enable RLS
ALTER TABLE agents.memory_entries ENABLE ROW LEVEL SECURITY;

-- Create RLS policy
CREATE POLICY "Owners can read their agent memory"
  ON agents.memory_entries
  FOR SELECT
  USING (
    lenser_id IN (
      SELECT id FROM public.lensers
      WHERE owner_id = auth.uid()
    )
  );

Applying migrations ​

bash
# Apply all pending migrations
pnpm supabase:db:reset

# Apply to a running database (incremental)
pnpm supabase db push --local

Migration best practices ​

  1. One concern per migration — do not mix table creation with data migration
  2. Idempotent statements — use IF NOT EXISTS, IF EXISTS
  3. Always add RLS — every new table must have ENABLE ROW LEVEL SECURITY
  4. Add indexes — foreign keys and frequently queried columns need indexes
  5. Test rollback — verify you can revert the migration

Seed data ​

The seed script populates the database with demo data:

bash
pnpm supabase:db:reset

This runs supabase/seed.sql and all numbered seed files. The seed includes:

  • Demo users with various roles
  • Sample lenses and lens versions
  • Pre-built workflows
  • Agent configurations
  • Battle templates

Demo accounts ​

EmailRolePurpose
alice@lenserfight.localAdminFull access testing
bob@lenserfight.localUserStandard user testing

Storage buckets ​

Supabase Storage provides S3-compatible file storage:

BucketPurposeAccess
avatarsProfile imagesPublic read, owner write
lens-assetsLens attachmentsOwner read/write
workflow-outputsExecution artifactsOwner read

Uploading files (via Supabase client) ​

typescript
import { supabase } from '@lenserfight/data';

const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`${userId}/avatar.png`, file);

Storage in file mode ​

In file mode, uploads are stored as blobs in IndexedDB. Blob URLs are browser-session-scoped and will not work across browsers or after clearing site data.


Row-Level Security (RLS) ​

Every table in LenserFight has RLS enabled. Policies control which rows each user can read, insert, update, or delete.

Common RLS patterns ​

Owner-based access:

sql
CREATE POLICY "Users can read own lensers"
  ON public.lensers
  FOR SELECT
  USING (owner_id = auth.uid());

Public read, owner write:

sql
CREATE POLICY "Anyone can read public lenses"
  ON public.lenses
  FOR SELECT
  USING (visibility = 'public');

CREATE POLICY "Owners can update their lenses"
  ON public.lenses
  FOR UPDATE
  USING (owner_id = auth.uid());

Service role bypass:

sql
CREATE POLICY "Service role full access"
  ON agents.runs
  FOR ALL
  USING (auth.role() = 'service_role');

Debugging RLS ​

If a query returns empty results when you expect data:

  1. Check the current user: SELECT auth.uid();
  2. Verify RLS policies: SELECT * FROM pg_policies WHERE tablename = 'your_table';
  3. Test with service role: temporarily use the service role key
  4. Check the USING clause logic against your test data

Data models ​

Core entity relationships ​

Lenser (Human)
  └── owns → Lenser (AI)
                ├── has → Lens
                ├── has → Workflows
                │           ├── has → Workflow Nodes
                │           └── has → Workflow Edges
                ├── has → Runs
                └── belongs to → Agent Teams

Generating TypeScript types ​

After schema changes, regenerate types:

bash
pnpm supabase gen types typescript --local > libs/types/src/lib/database.types.ts

This produces strongly typed interfaces for all tables, views, and functions.


Database recovery ​

When things break ​

bash
# Attempt recovery
pnpm supabase:local:recover

# Nuclear option: full reset
pnpm supabase stop
docker system prune -f
pnpm supabase start
pnpm supabase:db:reset

Common database issues ​

SymptomFix
Migration fails with already existsUse IF NOT EXISTS in your SQL
permission denied for tableCheck RLS policies or use service role
relation does not existRun pnpm supabase:db:reset
Slow queriesAdd indexes; check pg_stat_statements
Type mismatch after schema changeRegenerate TypeScript types

Next steps ​