๐ Supabase Annotations
A powerful, type-safe code generator for creating Supabase/PostgreSQL database schemas from Dart model classes. Build production-ready database schemas with Row Level Security (RLS), indexes, foreign keys, migrations, and table partitioning - all from your Dart code.
๐ Table of Contents
- โจ Features
- ๐ Quick Start
- ๐ Core Annotations
- ๐ง Configuration
- ๐๏ธ Column Types & Constraints
- ๐ Security & RLS Policies
- โก Performance & Indexing
- ๐ Migration Support
- ๐ฏ Advanced Examples
- ๐ Best Practices
- ๐ ๏ธ Development
- ๐ค Contributing
- ๐ License
โจ Features
๐๏ธ Schema Generation
- Type-Safe SQL Generation - Convert Dart classes to PostgreSQL schemas
- Full PostgreSQL Support - All column types, constraints, and features
- Automatic Documentation - Generate SQL comments from Dart documentation
๐ Security First
- Row Level Security (RLS) - Declarative RLS policy generation
- Fine-Grained Permissions - Control access at the row and column level
- Authentication Integration - Built-in Supabase auth helpers
โก Performance Optimization
- Smart Indexing - Automatic and custom index generation
- Query Optimization - Composite indexes and partial indexes
- Table Partitioning - Range and hash partitioning support
๐ Migration & Evolution
- Safe Schema Evolution - Multiple migration strategies
- Zero-Downtime Updates - ADD COLUMN and ALTER TABLE support
- Rollback Support - Safe migration with fallback options
๐ฏ Developer Experience
- IDE Integration - Full IntelliSense and code completion
- Comprehensive Validation - Catch errors at build time
- Rich Documentation - Inline help and examples
๐ Quick Start
1๏ธโฃ Installation
Add to your pubspec.yaml
:
dependencies:
supabase_annotations: ^1.1.1
dev_dependencies:
build_runner: ^2.4.8
source_gen: ^1.5.0
2๏ธโฃ Configuration
Create build.yaml
in your project root:
targets:
$default:
builders:
supabase_annotations|schema_builder:
enabled: true
generate_for:
include:
- lib/**.dart
- example/**.dart
exclude:
- lib/**.g.dart
- lib/**.schema.dart
options:
# ๐ Migration Strategy
migration_mode: 'createOrAlter' # Safe schema evolution
enable_column_adding: true # Add missing columns
generate_do_blocks: true # PostgreSQL DO blocks
# ๐ Security Configuration
enable_rls_by_default: false # RLS on all tables
# ๐ Code Generation
generate_comments: true # Include documentation
validate_schema: true # Schema validation
format_sql: true # Format output
3๏ธโฃ Define Your Model
import 'package:supabase_annotations/supabase_annotations.dart';
@DatabaseTable(
name: 'users',
enableRLS: true,
comment: 'Application users with authentication',
)
@RLSPolicy(
name: 'users_own_data',
type: RLSPolicyType.all,
condition: 'auth.uid() = id',
)
@DatabaseIndex(
name: 'users_email_idx',
columns: ['email'],
isUnique: true,
)
class User {
@DatabaseColumn(
type: ColumnType.uuid,
isPrimaryKey: true,
defaultValue: DefaultValue.generateUuid,
)
String? id;
@DatabaseColumn(
type: ColumnType.text,
isUnique: true,
isNullable: false,
)
late String email;
@DatabaseColumn(
type: ColumnType.varchar(100),
isNullable: false,
)
late String fullName;
@DatabaseColumn(
type: ColumnType.timestampWithTimeZone,
defaultValue: DefaultValue.currentTimestamp,
)
late DateTime createdAt;
@DatabaseColumn(
type: ColumnType.timestampWithTimeZone,
defaultValue: DefaultValue.currentTimestamp,
)
late DateTime updatedAt;
}
4๏ธโฃ Generate Schema
# Generate SQL schema files
dart run build_runner build
# Watch for changes and regenerate
dart run build_runner watch
5๏ธโฃ Generated Output
-- ๐ Generated: lib/models/user.schema.sql
-- ๐๏ธ Create table with RLS enabled
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
full_name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- ๐ Enable Row Level Security
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- ๐ก๏ธ Create RLS policy
CREATE POLICY users_own_data ON users
FOR ALL
USING (auth.uid() = id);
-- โก Create performance indexes
CREATE UNIQUE INDEX users_email_idx ON users(email);
-- ๐ Add table comment
COMMENT ON TABLE users IS 'Application users with authentication';
๐ Core Annotations
๐๏ธ @DatabaseTable
Configure table-level settings:
@DatabaseTable(
name: 'custom_table_name', // ๐ Custom table name (optional)
enableRLS: true, // ๐ Row Level Security
comment: 'Table description', // ๐ Documentation
partitionBy: RangePartition( // ๐ Table partitioning
columns: ['created_at']
),
)
class MyTable { }
๐ท๏ธ @DatabaseColumn
Define column properties:
@DatabaseColumn(
name: 'custom_column_name', // ๐ Custom column name
type: ColumnType.varchar(255), // ๐ฏ PostgreSQL type
isNullable: false, // โ NOT NULL constraint
isPrimaryKey: true, // ๐ Primary key
isUnique: true, // โญ Unique constraint
defaultValue: DefaultValue.currentTimestamp, // ๐ Default value
comment: 'Column description', // ๐ Documentation
checkConstraints: ['value > 0'], // โ
CHECK constraints
)
late String myField;
๐ @ForeignKey
Define relationships:
@ForeignKey(
table: 'users', // ๐ฏ Referenced table
column: 'id', // ๐ Referenced column
onDelete: ForeignKeyAction.cascade, // ๐๏ธ Delete behavior
onUpdate: ForeignKeyAction.restrict, // ๐ Update behavior
)
@DatabaseColumn(type: ColumnType.uuid)
late String userId;
โก @DatabaseIndex
Optimize performance:
// ๐ Composite index on table
@DatabaseIndex(
name: 'user_status_created_idx',
columns: ['status', 'created_at'],
type: IndexType.btree,
isUnique: false,
where: "status != 'deleted'", // ๐ฏ Partial index
)
class User { }
// ๐ Single column index
@DatabaseIndex(type: IndexType.hash)
@DatabaseColumn(type: ColumnType.text)
late String status;
๐ก๏ธ @RLSPolicy
Secure your data:
@RLSPolicy(
name: 'user_read_own', // ๐ Policy name
type: RLSPolicyType.select, // ๐ฏ Operation type
condition: 'auth.uid() = user_id', // ๐ Access condition
roles: ['authenticated'], // ๐ฅ Database roles
comment: 'Users can read their own data', // ๐ Documentation
)
class UserData { }
๐ง Configuration
๐ Configuration Options
Option | Type | Default | Description |
---|---|---|---|
migration_mode |
string | 'createOnly' |
Migration strategy |
enable_column_adding |
bool | true |
Add missing columns |
generate_do_blocks |
bool | true |
Use DO blocks for safety |
enable_rls_by_default |
bool | false |
RLS on all tables |
add_timestamps |
bool | false |
Auto-add timestamps |
use_explicit_nullability |
bool | false |
Explicit NULL/NOT NULL |
generate_comments |
bool | true |
Include documentation |
validate_schema |
bool | true |
Schema validation |
format_sql |
bool | true |
Format SQL output |
๐ฏ Environment-Specific Configurations
๐ง Development Setup:
options:
migration_mode: 'createOrAlter' # Safe evolution
enable_rls_by_default: false # Easier testing
generate_comments: true # Full docs
validate_schema: true # Catch errors
format_sql: true # Readable output
๐ Production Setup:
options:
migration_mode: 'createOrAlter' # Safe migrations
enable_column_adding: true # Allow evolution
generate_do_blocks: true # Extra safety
validate_schema: true # Strict validation
format_sql: true # Clean output
๐ค CI/CD Pipeline:
options:
migration_mode: 'createOnly' # Standard creation
validate_schema: true # Fail on errors
generate_comments: false # Minimal output
format_sql: true # Consistent format
๐๏ธ Column Types & Constraints
๐ PostgreSQL Column Types
๐ Text Types
ColumnType.text // TEXT
ColumnType.varchar(255) // VARCHAR(255)
ColumnType.char(10) // CHAR(10)
๐ข Numeric Types
ColumnType.integer // INTEGER
ColumnType.bigint // BIGINT
ColumnType.decimal(10, 2) // DECIMAL(10,2)
ColumnType.real // REAL
ColumnType.doublePrecision // DOUBLE PRECISION
ColumnType.serial // SERIAL
ColumnType.bigserial // BIGSERIAL
๐ Date/Time Types
ColumnType.timestamp // TIMESTAMP
ColumnType.timestampWithTimeZone // TIMESTAMPTZ
ColumnType.date // DATE
ColumnType.time // TIME
ColumnType.interval // INTERVAL
๐ฏ Special Types
ColumnType.uuid // UUID
ColumnType.boolean // BOOLEAN
ColumnType.json // JSON
ColumnType.jsonb // JSONB
ColumnType.bytea // BYTEA
ColumnType.inet // INET
ColumnType.macaddr // MACADDR
ColumnType.point // POINT
ColumnType.array(ColumnType.text) // TEXT[]
๐ Default Values
// ๐ Literal values
DefaultValue.none // NULL
DefaultValue.zero // 0
DefaultValue.one // 1
DefaultValue.emptyString // ''
DefaultValue.emptyArray // ARRAY[]
DefaultValue.emptyObject // '{}'
// โก Functions
DefaultValue.currentTimestamp // CURRENT_TIMESTAMP
DefaultValue.currentDate // CURRENT_DATE
DefaultValue.generateUuid // gen_random_uuid()
DefaultValue.autoIncrement // nextval(sequence)
// ๐ญ Factory methods
DefaultValue.string('value') // 'value'
DefaultValue.number(42) // 42
DefaultValue.boolean(true) // true
DefaultValue.expression('NOW()') // Custom expression
โ Constraints
@DatabaseColumn(
// ๐ Primary key
isPrimaryKey: true,
// โญ Unique constraint
isUnique: true,
// โ NOT NULL constraint
isNullable: false,
// โ
CHECK constraints
checkConstraints: [
'length(email) > 0',
'email ~* \'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\$\'',
],
)
late String email;
๐ Security & RLS Policies
๐ก๏ธ RLS Policy Types
RLSPolicyType.all // ๐ All operations (CRUD)
RLSPolicyType.select // ๐๏ธ Read operations only
RLSPolicyType.insert // โ Insert operations only
RLSPolicyType.update // โ๏ธ Update operations only
RLSPolicyType.delete // ๐๏ธ Delete operations only
๐ฏ Common RLS Patterns
๐ค User Owns Data
@RLSPolicy(
name: 'users_own_data',
type: RLSPolicyType.all,
condition: 'auth.uid() = user_id',
)
๐ข Multi-tenant Isolation
@RLSPolicy(
name: 'tenant_isolation',
type: RLSPolicyType.all,
condition: 'tenant_id = auth.jwt() ->> "tenant_id"',
)
๐ฅ Role-based Access
@RLSPolicy(
name: 'admin_full_access',
type: RLSPolicyType.all,
condition: 'auth.jwt() ->> "role" = "admin"',
roles: ['authenticated'],
)
@RLSPolicy(
name: 'user_read_only',
type: RLSPolicyType.select,
condition: 'auth.jwt() ->> "role" = "user"',
roles: ['authenticated'],
)
๐ Time-based Access
@RLSPolicy(
name: 'active_records_only',
type: RLSPolicyType.select,
condition: 'expires_at > NOW() AND is_active = true',
)
โก Performance & Indexing
๐ Index Types
IndexType.btree // ๐ณ B-tree (default, general purpose)
IndexType.hash // #๏ธโฃ Hash (equality only)
IndexType.gin // ๐ GIN (JSON, arrays, full-text)
IndexType.gist // ๐ฏ GiST (geometric, full-text)
IndexType.spgist // ๐ SP-GiST (space-partitioned)
IndexType.brin // ๐ BRIN (large ordered tables)
๐ Index Strategies
๐ Single Column Index
@DatabaseIndex(type: IndexType.btree)
@DatabaseColumn(type: ColumnType.text)
late String status;
๐ Composite Index
@DatabaseIndex(
name: 'user_activity_idx',
columns: ['user_id', 'created_at', 'activity_type'],
type: IndexType.btree,
)
๐ฏ Partial Index
@DatabaseIndex(
name: 'active_users_idx',
columns: ['email'],
where: "status = 'active' AND deleted_at IS NULL",
)
๐ Expression Index
@DatabaseIndex(
name: 'user_search_idx',
expression: "to_tsvector('english', name || ' ' || email)",
type: IndexType.gin,
)
๐ฑ JSON Index
@DatabaseIndex(
name: 'metadata_search_idx',
expression: "(metadata -> 'tags')",
type: IndexType.gin,
)
๐ Migration Support
๐ฏ Migration Modes
Mode | Description | Use Case |
---|---|---|
createOnly |
Standard CREATE TABLE | ๐ New projects |
createIfNotExists |
CREATE TABLE IF NOT EXISTS | ๐ Safe creation |
createOrAlter |
CREATE + ALTER for new columns | ๐ Schema evolution |
alterOnly |
Only ALTER TABLE statements | ๐ ๏ธ Existing schemas |
dropAndRecreate |
DROP and CREATE | ๐งช Development only |
๐ Migration Examples
๐ Adding New Column
// Add this field to existing User class
@DatabaseColumn(
type: ColumnType.integer,
defaultValue: DefaultValue.zero,
)
int? age;
Generated Migration:
-- ๐ Safe column addition
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'age'
) THEN
ALTER TABLE users ADD COLUMN age INTEGER DEFAULT 0;
END IF;
END $$;
๐ Adding Foreign Key
// Add relationship to existing table
@ForeignKey(
table: 'companies',
column: 'id',
onDelete: ForeignKeyAction.setNull,
)
@DatabaseColumn(type: ColumnType.uuid)
String? companyId;
Generated Migration:
-- ๐ Safe foreign key addition
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'company_id'
) THEN
ALTER TABLE users ADD COLUMN company_id UUID;
ALTER TABLE users ADD CONSTRAINT users_company_id_fkey
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE SET NULL;
END IF;
END $$;
๐ก๏ธ Safe Migration Practices
# ๐ฏ Recommended production configuration
options:
migration_mode: 'createOrAlter' # Safe evolution
enable_column_adding: true # Allow new columns
generate_do_blocks: true # Extra safety checks
validate_schema: true # Comprehensive validation
๐ฏ Advanced Examples
๐ข Multi-tenant SaaS Application
@DatabaseTable(
name: 'documents',
enableRLS: true,
comment: 'Multi-tenant document storage',
)
@RLSPolicy(
name: 'tenant_isolation',
type: RLSPolicyType.all,
condition: 'tenant_id = auth.jwt() ->> "tenant_id"',
)
@DatabaseIndex(
name: 'documents_tenant_created_idx',
columns: ['tenant_id', 'created_at'],
)
@DatabaseIndex(
name: 'documents_search_idx',
expression: "to_tsvector('english', title || ' ' || content)",
type: IndexType.gin,
)
class Document {
@DatabaseColumn(
type: ColumnType.uuid,
isPrimaryKey: true,
defaultValue: DefaultValue.generateUuid,
)
String? id;
@ForeignKey(
table: 'tenants',
column: 'id',
onDelete: ForeignKeyAction.cascade,
)
@DatabaseColumn(
type: ColumnType.uuid,
isNullable: false,
)
late String tenantId;
@DatabaseColumn(
type: ColumnType.text,
isNullable: false,
checkConstraints: ['length(title) > 0'],
)
late String title;
@DatabaseColumn(type: ColumnType.text)
String? content;
@DatabaseColumn(
type: ColumnType.jsonb,
defaultValue: DefaultValue.emptyObject,
)
Map<String, dynamic>? metadata;
@DatabaseColumn(
type: ColumnType.timestampWithTimeZone,
defaultValue: DefaultValue.currentTimestamp,
)
late DateTime createdAt;
}
๐ E-commerce System
@DatabaseTable(
name: 'orders',
enableRLS: true,
comment: 'Customer orders with audit trail',
)
@RLSPolicy(
name: 'customers_own_orders',
type: RLSPolicyType.select,
condition: 'customer_id = auth.uid()',
)
@RLSPolicy(
name: 'staff_manage_orders',
type: RLSPolicyType.all,
condition: 'auth.jwt() ->> "role" IN ("admin", "staff")',
)
@DatabaseIndex(
name: 'orders_customer_status_idx',
columns: ['customer_id', 'status', 'created_at'],
)
@DatabaseIndex(
name: 'orders_total_idx',
columns: ['total_amount'],
where: "status != 'cancelled'",
)
class Order {
@DatabaseColumn(
type: ColumnType.uuid,
isPrimaryKey: true,
defaultValue: DefaultValue.generateUuid,
)
String? id;
@ForeignKey(
table: 'customers',
column: 'id',
onDelete: ForeignKeyAction.restrict,
)
@DatabaseColumn(
type: ColumnType.uuid,
isNullable: false,
)
late String customerId;
@DatabaseColumn(
type: ColumnType.varchar(20),
defaultValue: DefaultValue.string('pending'),
checkConstraints: [
"status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')"
],
)
late String status;
@DatabaseColumn(
type: ColumnType.decimal(10, 2),
isNullable: false,
checkConstraints: ['total_amount >= 0'],
)
late double totalAmount;
@DatabaseColumn(
type: ColumnType.jsonb,
comment: 'Order line items with product details',
)
List<Map<String, dynamic>>? items;
@DatabaseColumn(
type: ColumnType.timestampWithTimeZone,
defaultValue: DefaultValue.currentTimestamp,
)
late DateTime createdAt;
@DatabaseColumn(
type: ColumnType.timestampWithTimeZone,
defaultValue: DefaultValue.currentTimestamp,
)
late DateTime updatedAt;
}
๐ Analytics & Logging
@DatabaseTable(
name: 'events',
comment: 'Application event tracking',
partitionBy: RangePartition(columns: ['created_at']),
)
@DatabaseIndex(
name: 'events_type_created_idx',
columns: ['event_type', 'created_at'],
)
@DatabaseIndex(
name: 'events_user_session_idx',
columns: ['user_id', 'session_id'],
where: "user_id IS NOT NULL",
)
@DatabaseIndex(
name: 'events_properties_idx',
expression: "(properties -> 'category')",
type: IndexType.gin,
)
class Event {
@DatabaseColumn(
type: ColumnType.uuid,
isPrimaryKey: true,
defaultValue: DefaultValue.generateUuid,
)
String? id;
@DatabaseColumn(
type: ColumnType.varchar(50),
isNullable: false,
)
late String eventType;
@DatabaseColumn(type: ColumnType.uuid)
String? userId;
@DatabaseColumn(type: ColumnType.uuid)
String? sessionId;
@DatabaseColumn(
type: ColumnType.jsonb,
defaultValue: DefaultValue.emptyObject,
)
Map<String, dynamic>? properties;
@DatabaseColumn(
type: ColumnType.inet,
comment: 'Client IP address',
)
String? ipAddress;
@DatabaseColumn(
type: ColumnType.text,
comment: 'User agent string',
)
String? userAgent;
@DatabaseColumn(
type: ColumnType.timestampWithTimeZone,
defaultValue: DefaultValue.currentTimestamp,
isNullable: false,
)
late DateTime createdAt;
}
๐ Best Practices
๐๏ธ Schema Design
โ DO:
- Use descriptive, meaningful names
- Follow PostgreSQL naming conventions (snake_case)
- Keep names under 63 characters
- Add comprehensive comments and documentation
- Use appropriate column types for your data
โ DON'T:
- Use reserved keywords as names
- Create overly complex nested structures
- Forget to add indexes on frequently queried columns
- Skip validation constraints
๐ Security Guidelines
โ DO:
- Always enable RLS on tables with sensitive data
- Use specific, restrictive policy conditions
- Test policies thoroughly with different user roles
- Document security requirements and assumptions
- Use parameterized conditions to prevent injection
โ DON'T:
- Rely solely on application-level security
- Create overly permissive policies
- Forget to test edge cases in policy conditions
- Hardcode user IDs in policies
โก Performance Optimization
โ DO:
- Add indexes on frequently queried columns
- Use composite indexes for multi-column queries
- Consider partial indexes for filtered queries
- Use appropriate index types for your use case
- Monitor query performance regularly
โ DON'T:
- Create too many indexes (impacts write performance)
- Index every column "just in case"
- Forget to maintain statistics on large tables
- Ignore query execution plans
๐ Migration Management
โ DO:
- Use migration modes for schema evolution
- Test migrations on staging data first
- Plan for rollback scenarios
- Document breaking changes thoroughly
- Use
createOrAlter
mode for production
โ DON'T:
- Drop tables or columns without backup
- Skip testing migrations
- Apply untested migrations to production
- Forget to version your schema changes
๐ ๏ธ Development
๐ Getting Started
# Clone the repository
git clone https://github.com/ahmtydn/supabase_annotations.git
cd supabase_annotations
# Install dependencies
dart pub get
# Run tests
dart test
# Run analysis
dart analyze
# Generate documentation
dart doc
๐งช Running Examples
# Navigate to examples
cd example
# Generate schemas for all examples
dart run build_runner build
# View generated SQL files
ls lib/*.schema.sql
๐ Project Structure
lib/
โโโ builder.dart # Build configuration
โโโ supabase_annotations.dart # Public API
โโโ src/
โโโ annotations/ # Annotation definitions
โ โโโ database_column.dart
โ โโโ database_index.dart
โ โโโ database_table.dart
โ โโโ foreign_key.dart
โ โโโ rls_policy.dart
โโโ generators/ # Code generation logic
โ โโโ schema_generator.dart
โโโ models/ # Data models
โโโ column_types.dart
โโโ default_values.dart
โโโ foreign_key_actions.dart
โโโ index_types.dart
โโโ migration_config.dart
โโโ partition_strategy.dart
โโโ table_constraints.dart
โโโ validators.dart
๐ค Contributing
We welcome contributions! Here's how you can help:
๐ Bug Reports
- Use the issue tracker
- Include a minimal reproduction case
- Provide environment details (Dart version, OS, etc.)
๐ก Feature Requests
- Check existing discussions
- Explain the use case and benefits
- Consider implementation complexity
๐ง Pull Requests
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Add tests for new functionality
- Ensure all tests pass (
dart test
) - Run analysis (
dart analyze
) - Commit changes (
git commit -m 'Add amazing feature'
) - Push to branch (
git push origin feature/amazing-feature
) - Submit a pull request
๐ Development Guidelines
- Follow the existing code style
- Add comprehensive tests
- Update documentation
- Include examples for new features
- Ensure backward compatibility
๐ Support & Community
๐ Documentation
๐ฌ Community
๐ Need Help?
- Check the FAQ
- Search existing issues
- Ask in discussions
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments
- Supabase Team - For creating an amazing platform
- Dart Team - For excellent tooling and language features
- PostgreSQL Community - For the world's most advanced open source database
- Contributors - For making this project better
๐ Show Your Support
If this project helped you, please consider:
- โญ Star the repository
- ๐ Share with your team
- ๐ Report issues
- ๐ก Suggest improvements
- ๐ค Contribute code
Built with โค๏ธ for the Supabase and Dart communities
๐ Website โข ๐ Docs โข ๐ฌ Community
Libraries
- builder
- Builder configuration for the Supabase schema generator.
- supabase_annotations
- A comprehensive code generator for creating Supabase/PostgreSQL database schemas from Dart model classes.