| name | supabase-migration |
| description | Activates when working with database migrations, RLS policies, or Supabase schema changes.
Use this skill for: creating migrations, writing RLS policies, handling PostGIS geometry,
creating indexes, managing materialized views, and generating TypeScript types.
Keywords: migration, supabase, RLS, policy, PostGIS, geometry, schema, table, index
|
Supabase Migration Skill
This skill provides guidance for database migrations and schema management in Landbruget.dk.
Activation Context
This skill activates when:
- Creating database migrations
- Writing RLS (Row Level Security) policies
- Working with PostGIS/geometry columns
- Creating indexes for performance
- Managing materialized views
- Generating TypeScript types from schema
Environment Setup
supabase status
supabase link --project-ref <project-ref>
supabase db pull
Creating Migrations
Standard Migration
supabase migration new <migration_name>
supabase migration new add_farm_statistics_table
This creates: supabase/migrations/[timestamp]_add_farm_statistics_table.sql
Migration Template
CREATE TABLE IF NOT EXISTS [table_name] (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
cvr_number VARCHAR(8) NOT NULL,
name TEXT NOT NULL,
CONSTRAINT valid_cvr CHECK (cvr_number ~ '^\d{8}$')
);
ALTER TABLE [table_name] ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow public read access"
ON [table_name]
FOR SELECT
USING (true);
CREATE INDEX idx_[table]_cvr ON [table_name] (cvr_number);
CREATE INDEX idx_[table]_created ON [table_name] (created_at);
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER [table]_updated_at
BEFORE UPDATE ON [table_name]
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE [table_name] IS '[Description of table purpose]';
COMMENT ON COLUMN [table_name].[column] IS '[Description]';
RLS Policies
Common Policy Patterns
Public Read Access (most common for Landbruget.dk):
CREATE POLICY "Allow public read access"
ON [table_name]
FOR SELECT
USING (true);
Authenticated Users Only:
CREATE POLICY "Allow authenticated read"
ON [table_name]
FOR SELECT
TO authenticated
USING (true);
Owner-Only Access:
CREATE POLICY "Users can only see own data"
ON [table_name]
FOR SELECT
USING (auth.uid() = user_id);
Role-Based Access:
CREATE POLICY "Admins can do everything"
ON [table_name]
FOR ALL
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid()
AND role = 'admin'
)
);
PostGIS / Geometry
Creating Geometry Columns
CREATE EXTENSION IF NOT EXISTS postgis;
ALTER TABLE [table_name]
ADD COLUMN geom GEOMETRY(Point, 4326);
ALTER TABLE [table_name]
ADD COLUMN boundary GEOMETRY(Polygon, 4326);
CREATE INDEX idx_[table]_geom ON [table_name] USING GIST (geom);
Coordinate Systems
| EPSG | Name | Use Case |
|---|
| 4326 | WGS84 | Storage standard |
| 25832 | UTM 32N | Danish data input |
| 3857 | Web Mercator | Display/maps |
Conversion:
SELECT ST_Transform(geom, 4326) FROM ...
SELECT ST_SetSRID(geom, 4326) FROM ...
Common Spatial Queries
SELECT * FROM farms
WHERE ST_Within(geom, (SELECT boundary FROM regions WHERE name = 'Jutland'));
SELECT *, ST_Distance(geom::geography, point::geography) as distance
FROM farms
WHERE ST_DWithin(geom::geography, point::geography, 10000)
ORDER BY distance;
SELECT ST_Centroid(boundary) FROM fields;
Indexes
When to Create Indexes
- Columns used in WHERE clauses frequently
- Columns used in JOIN conditions
- Columns used in ORDER BY
- Foreign key columns
Index Types
CREATE INDEX idx_name ON table (column);
CREATE INDEX idx_geom ON table USING GIST (geom);
CREATE INDEX idx_data ON table USING GIN (data_jsonb);
CREATE INDEX idx_active ON table (column) WHERE is_active = true;
Required Indexes for Landbruget.dk
CREATE INDEX idx_[table]_cvr ON [table] (cvr_number);
CREATE INDEX idx_[table]_chr ON [table] (chr_number);
CREATE INDEX idx_[table]_bfe ON [table] (bfe_number);
CREATE INDEX idx_[table]_geom ON [table] USING GIST (geom);
Materialized Views
For complex aggregations that don't need real-time updates:
CREATE MATERIALIZED VIEW farm_statistics AS
SELECT
cvr_number,
COUNT(*) as field_count,
SUM(area_ha) as total_area,
array_agg(DISTINCT crop_type) as crop_types
FROM fields
GROUP BY cvr_number;
CREATE UNIQUE INDEX idx_farm_stats_cvr ON farm_statistics (cvr_number);
REFRESH MATERIALIZED VIEW CONCURRENTLY farm_statistics;
Applying Migrations
supabase db push
supabase db reset
supabase migration list
Generate TypeScript Types
supabase gen types typescript --local > frontend/src/types/supabase.ts
supabase gen types typescript --project-id <project-id> > frontend/src/types/supabase.ts
Rollback Strategies
For simple changes, create a new migration:
DROP TABLE IF EXISTS [table_name];
For complex rollbacks, keep down migrations:
CREATE TABLE ...
DROP TABLE ...
Migration Checklist
Before marking migration work complete:
Troubleshooting
Migration Syntax Error
supabase db lint
RLS Blocking Access
SET LOCAL ROLE postgres;
SELECT * FROM [table];
Missing PostGIS
CREATE EXTENSION IF NOT EXISTS postgis;