TamborraData uses Supabase PostgreSQL with Row Level Security (RLS) to ensure data protection at the database level. The application is read-only from the web interface, with write access reserved for the data pipeline.
Purpose: Stores all statistical data aggregated by category, scope, and year.
CREATE TABLE statistics ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), category text NOT NULL, scope text NOT NULL, year text NOT NULL, public_data jsonb, full_data jsonb, summary text, created_at timestamp DEFAULT now() NOT NULL, UNIQUE (category, scope, year));
Columns:
id: Primary key (UUID)
category: Type of statistic (e.g., “top-names”, “top-schools”)
scope: Data scope (“year” or “global”)
year: Year or “global” for aggregated data
public_data: Public-facing data (JSONB for flexibility)
full_data: Complete data (includes sensitive info)
summary: Human-readable description
created_at: Creation timestamp
Unique Constraint:(category, scope, year) ensures no duplicates
JSONB columns allow flexible schema while maintaining queryability. PostgreSQL can index and query JSONB efficiently.
Purpose: Tracks which years have complete data ready for display.
CREATE TABLE available_years ( year text UNIQUE PRIMARY KEY, is_ready boolean DEFAULT FALSE NOT NULL, created_at timestamp DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL);
Columns:
year: Year identifier (e.g., “2024”, “global”)
is_ready: Whether data is complete and ready
created_at: When year was added
updated_at: Last modification timestamp
Usage:
// Frontend queries only ready yearsSELECT year FROM available_years WHERE is_ready = true;
Purpose: Global system status flag for coordinating updates.
CREATE TABLE sys_status ( id integer PRIMARY KEY DEFAULT 1, is_updating boolean DEFAULT FALSE NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, notes text);
Columns:
id: Always 1 (singleton pattern)
is_updating: Whether data pipeline is running
updated_at: Last status change
notes: Optional status message
Singleton Pattern:
-- Only one row ever existsINSERT INTO sys_status (id, is_updating, notes)VALUES (1, false, 'Sistema iniciado')ON CONFLICT (id) DO NOTHING;
This table prevents the frontend from showing stale data during pipeline updates.
-- Anonymous users (web app) can only readCREATE POLICY "Anon read access on statistics"ON statisticsFOR SELECTTO anonUSING (true);CREATE POLICY "Anon read access on available_years"ON available_yearsFOR SELECTTO anonUSING (true);CREATE POLICY "Anon read access on sys_status"ON sys_statusFOR SELECTTO anonUSING (true);
USING (true) means all rows are visible, but only for SELECT. INSERT, UPDATE, DELETE are denied by default.
Write access is granted to the service role (used by data pipeline):
-- Service role has full access-- (Implicit: service_role bypasses RLS by default)-- Pipeline uses SUPABASE_SERVICE_ROLE_KEY-- Web app uses SUPABASE_ANON_KEY
Environment separation:
# Web app (.env)SUPABASE_ANON_KEY=eyJ... # Read-only# Pipeline (.env)SUPABASE_SERVICE_KEY=eyJ... # Read-write
Defense in Depth
Even if API is compromised, database enforces read-only access. Attackers cannot modify data.
No Application Logic
Security enforced by PostgreSQL, not application code. Cannot be bypassed.
Audit Trail
All access attempts logged by PostgreSQL. Can track unauthorized access.
Performance
PostgreSQL optimizes queries with RLS natively. No performance penalty.
-- Composite index for common query patternCREATE INDEX idx_statistics_year_categoryON statistics (year, category);-- Index on is_ready for filteringCREATE INDEX idx_years_readyON available_years (is_ready) WHERE is_ready = true;-- GIN index for JSONB queriesCREATE INDEX idx_statistics_public_dataON statistics USING GIN (public_data);-- Index on created_at for time-based queriesCREATE INDEX idx_statistics_createdON statistics (created_at DESC);
// migrations/001_initial_schema.sqlCREATE TABLE statistics ( -- schema definition);-- migrations/002_add_indexes.sqlCREATE INDEX idx_statistics_yearON statistics (year);// Track migrations in version control
Recovery Plan:
Restore from latest Supabase backup
If backup fails, restore from manual SQL dump
Re-run pipeline to regenerate data
Verify data integrity with checksums
Test your backups regularly! A backup is only useful if it can be restored.
-- ❌ Bad: Select everythingSELECT * FROM statistics;-- ✅ Good: Select only needed columnsSELECT category, public_data FROM statisticsWHERE year = '2024'LIMIT 30;
JSONB Performance
Use GIN indexes for JSONB queries
Avoid deep nesting (>3 levels)
Consider denormalization for hot paths
Use jsonb_array_elements for array queries
-- Create GIN indexCREATE INDEX ON statistics USING GIN (public_data);-- Efficient JSONB querySELECT * FROM statisticsWHERE public_data @> '{"rank": 1}';
Connection Pooling
Supabase handles pooling automatically
Use persistent connections in serverless
Monitor connection count in Supabase dashboard
Set appropriate timeout values
Supabase free tier allows 500 concurrent connections. Upgrade if you need more.
Monitoring
Monitor these metrics in Supabase dashboard:
Query performance (slow queries)
Database size and growth
Connection count
Cache hit ratio
Index usage
-- Find slow queriesSELECT * FROM pg_stat_statements ORDER BY total_time DESCLIMIT 10;