294 lines
11 KiB
Plaintext
294 lines
11 KiB
Plaintext
---
|
|
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/<number>-<name>.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 --filter web supabase:db: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');
|
|
``` |