--- description: Detailed Database Schema and Architecture globs: alwaysApply: true --- # Database Rules ## Database Architecture - Supabase uses Postgres - We strive to create a safe, robust, performant schema - Accounts are the general concept of a user account, defined by the having the same ID as Supabase Auth's users (personal). They can be a team account or a personal account. - Generally speaking, other tables will be used to store data related to the account. For example, a table `notes` would have a foreign key `account_id` to link it to an account. ## Schemas - The DB schemas are available at `apps/web/supabase/schemas` - To edit the DB schema, we can either change the schema files, or created new ones - To create a new schema, create a file at `apps/web/supabase/schemas/-.sql` ## Migrations - After creating a schema, we can create a migration - Use the command `pnpm --filter web supabase:db:diff` for creating migrations from schemas - After generating a migration, reset the database for applying the changes using the command `pnpm run supabase:web:reset` ## Security & RLS - Using RLS, we must ensure that only the account owner can access the data. Always write safe RLS policies and ensure that the policies are enforced. - Unless specified, always enable RLS when creating a table. Propose the required RLS policies ensuring the safety of the data. - Always consider any required constraints and triggers are in place for data consistency - Always consider the compromises you need to make and explain them so I can make an educated decision. Follow up with the considerations make and explain them. - Always consider the security of the data and explain the security implications of the data. - Always use Postgres schemas explicitly (e.g., `public.accounts`) - Consider the required compromises between simplicity, functionality and developer experience. However, never compromise on security, which is paramount and fundamental. - Use existing helper functions for access control instead of making your own queries, unless unavailable ## Schema Overview Makerkit uses a Supabase Postgres database with a well-defined schema focused on multi-tenancy through the concepts of accounts (both personal and team) and robust permission systems. ### Database Schema 1. Enums [01-enums.sql](mdc:apps/web/supabase/schemas/01-enums.sql) 2. Config [02-config.sql](mdc:apps/web/supabase/schemas/02-config.sql) 3. Accounts [03-accounts.sql](mdc:apps/web/supabase/schemas/03-accounts.sql) 4. Roles [04-roles.sql](mdc:apps/web/supabase/schemas/04-roles.sql) 5. Memberships [05-memberships.sql](mdc:apps/web/supabase/schemas/05-memberships.sql) 6. Roles Permissions [06-roles-permissions.sql](mdc:apps/web/supabase/schemas/06-roles-permissions.sql) 7. Invitations [07-invitations.sql](mdc:apps/web/supabase/schemas/07-invitations.sql) 8. Billing Customers [08-billing-customers.sql](mdc:apps/web/supabase/schemas/08-billing-customers.sql) 9. Subscriptions [09-subscriptions.sql](mdc:apps/web/supabase/schemas/09-subscriptions.sql) 10. Orders [10-orders.sql](mdc:apps/web/supabase/schemas/10-orders.sql) 11. Notifications [11-notifications.sql](mdc:apps/web/supabase/schemas/11-notifications.sql) 12. One Time Tokens [12-one-time-tokens.sql](mdc:apps/web/supabase/schemas/12-one-time-tokens.sql) 13. Multi Factor Auth [13-mfa.sql](mdc:apps/web/supabase/schemas/13-mfa.sql) 14. Super Admin [14-super-admin.sql](mdc:apps/web/supabase/schemas/14-super-admin.sql) 15. Account Views [15-account-views.sql](mdc:apps/web/supabase/schemas/15-account-views.sql) 16. Storage [16-storage.sql](mdc:apps/web/supabase/schemas/16-storage.sql) ## Database Best Practices ### Inferring Database types Fetch auto-generated data types using the `@kit/supabase/database` import. Do not write types manually if the shape is the same as the one from the database row. ```tsx import { Tables } from '@kit/supabase/database'; // public.accounts type Account = Tables<'accounts'>; // public.subscriptions type Subscription = Tables<'subscriptions'>; // public.notifications type Notification = Tables<'notifications'>; // ... ``` ### Security - **Always enable RLS** on new tables unless explicitly instructed otherwise - **Create proper RLS policies** for all CRUD operations following existing patterns - **Always associate data with accounts** using a foreign key to ensure proper access control - **Use explicit schema references** (`public.table_name` not just `table_name`) - **Private schema**: Place internal functions in the `kit` schema - **Search Path**: Always set search path to '' when defining functions - **Security Definer**: Do not use `security definer` functions unless stricly required ### Data Access Patterns - Use `public.has_role_on_account(account_id, role?)` to check membership - Use `public.has_permission(user_id, account_id, permission)` for permission checks - Use `public.is_account_owner(account_id)` to identify account ownership ### SQL Coding Style - Use explicit transactions for multi-step operations - Follow existing naming conventions: - Tables: snake_case, plural nouns (`accounts`, `subscriptions`) - Functions: snake_case, verb phrases (`create_team_account`, `verify_nonce`) - Triggers: descriptive action names (`set_slug_from_account_name`) - Document functions and complex SQL with comments - Use parameterized queries to prevent SQL injection ### Common Patterns - **Account Lookup**: Typically by `id` (UUID) or `slug` (for team accounts) - **Permission Check**: Always verify proper permissions before mutations - **Timestamp Automation**: Use the `trigger_set_timestamps()` function - **User Tracking**: Use the `trigger_set_user_tracking()` function - **Configuration**: Use `is_set(field_name)` to check enabled features ## Best Practices for Database Code ### 1. RLS Policy Management #### Always Enable RLS Always enable RLS for your tables unless you have a specific reason not to. ```sql ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY; ``` #### Use Helper Functions to validate permissions and access control Use the existing structure for policies: ```sql -- SELECT policy CREATE POLICY "my_table_read" ON public.my_table FOR SELECT TO authenticated USING ( account_id = (select auth.uid()) OR public.has_role_on_account(account_id) ); -- INSERT/UPDATE/DELETE policies follow similar patterns ``` When using RLS at team-account level, use `public.has_role_on_account(account_id)` for a generic check to understand if a user is part of a team. When using RLS at user-account level, use `account_id = (select auth.uid())`. When an entity can belong to both, use both. When requiring a specific role, use the role parameter `public.has_role_on_account(account_id, 'owner')` ### 2. Account Association - **Associate Data with Accounts**: Always link data to accounts using a foreign key: ```sql CREATE TABLE if not exists public.my_data ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID REFERENCES public.accounts(id) ON DELETE CASCADE NOT NULL, /* other fields */ ); ``` ### 3. Permission System - **Use the Permission System**: Leverage the built-in permission system for access control: ```sql -- Check if a user has a specific permission SELECT public.has_permission( auth.uid(), account_id, 'my.permission'::public.app_permissions ); ``` ### 4. Schema Organization - **Use Schemas Explicitly**: Always use schema prefixes explicitly: ```sql -- Good SELECT * FROM public.accounts; -- Avoid SELECT * FROM accounts; ``` - **Put Internal Functions in 'kit' Schema**: Use the 'kit' schema for internal helper functions ```sql CREATE OR REPLACE FUNCTION kit.my_helper_function() RETURNS void AS $$ -- function body $$ LANGUAGE plpgsql; ``` ### 5. Types and Constraints - **Use Enums for Constrained Values**: Create and use enum types for values with a fixed set: ```sql CREATE TYPE public.my_status AS ENUM('active', 'inactive', 'pending'); CREATE TABLE if not exists public.my_table ( status public.my_status NOT NULL DEFAULT 'pending' ); ``` - **Apply Appropriate Constraints**: Use constraints to ensure data integrity: ```sql CREATE TABLE if not exists public.my_table ( email VARCHAR(255) NOT NULL CHECK (email ~* '^.+@.+\..+$'), count INTEGER NOT NULL CHECK (count >= 0), /* other fields */ ); ``` ### 6. Authentication and User Management - **Use Supabase Auth**: Leverage auth.users for identity management - **Handle User Creation**: Use triggers like `kit.setup_new_user` to set up user data after registration ### 7. Function Security - **Apply Security Definer Carefully**: For functions that need elevated privileges: ```sql CREATE OR REPLACE FUNCTION public.my_function() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ -- function body $$; ``` - **Set Proper Function Permissions**: ```sql GRANT EXECUTE ON FUNCTION public.my_function() TO authenticated, service_role; ``` ### 8. Error Handling and Validation - **Use Custom Error Messages**: Return meaningful errors: ```sql IF NOT validation_passed THEN RAISE EXCEPTION 'Validation failed: %', error_message; END IF; ``` ### 9. Triggers for Automation - **Use Triggers for Derived Data**: Automate updates to derived fields: ```sql CREATE TRIGGER update_timestamp BEFORE UPDATE ON public.my_table FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamps(); ``` ### 10. View Structure for Commonly Used Queries - **Create Views for Complex Joins**: As done with `user_account_workspace` ```sql CREATE OR REPLACE VIEW public.my_view WITH (security_invoker = true) AS SELECT ... ``` You always must use `(security_invoker = true)` for views. ## Key Functions to Know 1. **Account Access** - `public.has_role_on_account(account_id, account_role)` - `public.is_account_owner(account_id)` - `public.is_team_member(account_id, user_id)` 2. **Permissions** - `public.has_permission(user_id, account_id, permission_name)` - `public.has_more_elevated_role(target_user_id, target_account_id, role_name)` 3. **Team Management** - `public.create_team_account(account_name)` 4. **Billing & Subscriptions** - `public.has_active_subscription(target_account_id)` 5. **One-Time Tokens** - `public.create_nonce(...)` - `public.verify_nonce(...)` - `public.revoke_nonce(...)` 6. **Super Admins** - `public.is_super_admin()` 7. **MFA**: - `public.is_aal2()` - `public.is_mfa_compliant()` ## Configuration Control - **Use the `config` Table**: The application has a central configuration table - **Check Features with `public.is_set(field_name)`**: ```sql -- Check if team accounts are enabled SELECT public.is_set('enable_team_accounts'); ```