Creating Custom Permission Seeds for Supamode
Understanding how to create a seed for your Supamode application
In this comprehensive guide, we'll explore how to create tailored permission structures that perfectly match your organization's unique access control requirements.
Key Learning Objectives:
- Master the Supamode seed generation architecture
- Build custom permission hierarchies from scratch
- Implement business-specific access control patterns
- Deploy scalable, maintainable permission structures
Understanding Supamode's Seed Architecture 🏗️
Before diving into custom implementations, let's understand how Supamode's seed system works under the hood.
The default seed schema is a simplified version of the Makerkit SaaS Starter Kit. Therefore, the default seed file is based off of that.
The recommendation is that instead you modify the default seed with mock data created based off your own schema.
The Seed Generator Pattern
Supamode uses a builder pattern to construct permission structures programmatically:
// Core Architectureconst app = new SupamodeSeedGenerator();// 1. Create entitiesconst role = Role.create({ app, id: 'custom_role', config: {...} });const permission = Permission.create({ app, id: 'custom_perm', config: {...} });// 2. Establish relationshipsrole.addPermission(permission);// 3. Generate SQLconst sql = app.generateSql();
Key Benefits:
- Type Safety - TypeScript ensures correct configuration
- Relationship Validation - Prevents orphaned permissions
- SQL Generation - Automatically creates deployment scripts
- Reusability - Seed templates can be shared across projects
Seed File Structure
Every seed file follows this consistent pattern:
// 1. Import the generator classesimport { Account, Permission, PermissionGroup, Role, SupamodeSeedGenerator } from '../generator';// 2. Initialize the generatorconst app = new SupamodeSeedGenerator();// 3. Define your structure (accounts, roles, permissions, groups)// ... your custom definitions ...// 4. Export the configured generatorexport default app;
Building Your First Custom Seed 🚀
Let's create a practical example: a content management system with editorial workflows.
Step 1: Define Your Business Requirements
Scenario: Online magazine with the following roles:
- Editor-in-Chief - Full editorial control
- Senior Editor - Manages content and junior staff
- Staff Writer - Creates and edits own content
- Freelancer - Limited content creation
- Subscriber - Read-only access to published content
Step 2: Create the Seed File
- Create
packages/schema/src/templates/editorial-seed.ts
- Add the following code to define roles, permissions, and groups:
import { Account, Permission, PermissionGroup, Role, SupamodeSeedGenerator,} from '../generator';const app = new SupamodeSeedGenerator();// ========================================// SECTION 1: DEFINE ROLES// ========================================const editorInChiefRole = Role.create({ app, id: 'editor_in_chief', config: { name: 'Editor-in-Chief', description: 'Full editorial control and staff management', priority: 95, // High authority metadata: { department: 'Editorial', can_publish: true } },});const seniorEditorRole = Role.create({ app, id: 'senior_editor', config: { name: 'Senior Editor', description: 'Content oversight and team coordination', priority: 80, metadata: { department: 'Editorial', can_assign_stories: true } },});const staffWriterRole = Role.create({ app, id: 'staff_writer', config: { name: 'Staff Writer', description: 'Full-time content creator', priority: 60, metadata: { employment_type: 'full_time' } },});const freelancerRole = Role.create({ app, id: 'freelancer', config: { name: 'Freelancer', description: 'Contract-based content contributor', priority: 40, metadata: { employment_type: 'contract' } },});const subscriberRole = Role.create({ app, id: 'subscriber', config: { name: 'Subscriber', description: 'Registered reader with premium access', priority: 20 },});
Step 3: Create Business-Specific Permissions
Now, let's define the permissions that align with our editorial workflow.
// ========================================// SECTION 2: CONTENT PERMISSIONS// ========================================// Article Managementconst createArticles = Permission.createDataPermission({ app, id: 'create_articles', name: 'Create Articles', description: 'Can create new article drafts', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'insert'});const editOwnArticles = Permission.createDataPermission({ app, id: 'edit_own_articles', name: 'Edit Own Articles', description: 'Can edit articles they authored', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'update', conditions: { author_id: '$CURRENT_USER_ID' } // Row-level security});const editAllArticles = Permission.createDataPermission({ app, id: 'edit_all_articles', name: 'Edit All Articles', description: 'Can edit any article regardless of author', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'update'});const publishArticles = Permission.createDataPermission({ app, id: 'publish_articles', name: 'Publish Articles', description: 'Can change article status to published', scope: 'column', schema_name: 'public', table_name: 'articles', column_name: 'status', action: 'update', conditions: { status: { $in: ['draft', 'review', 'published'] } }});// User Management Permissionsconst manageEditorialStaff = Permission.createSystemPermission({ app, id: 'manage_editorial_staff', resource: 'account', action: 'update', name: 'Manage Editorial Staff', description: 'Can update staff member accounts and assignments'});const viewAnalytics = Permission.createDataPermission({ app, id: 'view_analytics', name: 'View Analytics', description: 'Access to content performance metrics', scope: 'table', schema_name: 'public', table_name: 'article_analytics', action: 'select'});
Step 4: Organize with Permission Groups
Permission groups help us manage related permissions together, making it easier to assign them to roles.
// ========================================// SECTION 3: PERMISSION GROUPS// ========================================const editorialManagementGroup = PermissionGroup.create({ app, id: 'editorial_management', config: { name: 'Editorial Management', description: 'Full editorial oversight and staff management', },});editorialManagementGroup.addPermissions([ createArticles, editAllArticles, publishArticles, manageEditorialStaff, viewAnalytics]);const contentCreationGroup = PermissionGroup.create({ app, id: 'content_creation', config: { name: 'Content Creation', description: 'Core content creation and editing capabilities', },});contentCreationGroup.addPermissions([ createArticles, editOwnArticles, viewAnalytics]);const readOnlyGroup = PermissionGroup.create({ app, id: 'subscriber_access', config: { name: 'Subscriber Access', description: 'Read access to published content', },});// Only published articles for subscribersconst readPublishedArticles = Permission.createDataPermission({ app, id: 'read_published_articles', name: 'Read Published Articles', description: 'Can view published articles only', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'select', conditions: { status: 'published' }});readOnlyGroup.addPermissions([readPublishedArticles]);
Step 5: Assign Groups to Roles
Now that we have our roles, permissions, and groups defined, we can assign them to the appropriate roles.
// ========================================// SECTION 4: ROLE ASSIGNMENTS// ========================================// Editor-in-Chief gets full editorial managementeditorInChiefRole.addPermissionGroup({ group: editorialManagementGroup });// Senior Editors get content creation + some management permissionsseniorEditorRole.addPermissionGroup({ group: contentCreationGroup });seniorEditorRole.addPermission(editAllArticles); // Can edit any articleseniorEditorRole.addPermission(publishArticles); // Can publish// Staff Writers get core content creationstaffWriterRole.addPermissionGroup({ group: contentCreationGroup });// Freelancers get limited content creation (no analytics)freelancerRole.addPermission(createArticles);freelancerRole.addPermission(editOwnArticles);// Subscribers get read-only accesssubscriberRole.addPermissionGroup({ group: readOnlyGroup });
Step 6: Create Test Accounts
We can now create test accounts for our test users. We can either reference existing accounts by passing their IDs, or create new ones directly in the seed file.
- if we pass an
ID
- we expect the user to exist in the database - if the
ID
does not exist, we will create a new account with the provided metadata.
// ========================================// SECTION 5: TEST ACCOUNTS// ========================================const editorAccount = Account.create({ app, id: '550e8400-e29b-41d4-a716-446655440001', config: { metadata: { display_name: 'Sarah Johnson', email: 'sarah.johnson@magazine.com', department: 'Editorial', hire_date: '2023-01-15' }, },});const writerAccount = Account.create({ app, id: '550e8400-e29b-41d4-a716-446655440002', config: { metadata: { display_name: 'Mike Chen', email: 'mike.chen@magazine.com', department: 'Editorial', specialization: 'Technology' }, },});const freelancerAccount = Account.create({ app, id: '550e8400-e29b-41d4-a716-446655440003', config: { metadata: { display_name: 'Alex Rivera', email: 'alex.rivera@freelance.com', contract_type: 'per_article' }, },});// Assign roles to accountseditorAccount.assignRole(editorInChiefRole);writerAccount.assignRole(staffWriterRole);freelancerAccount.assignRole(freelancerRole);export default app;
Advanced Permission Patterns 🎯
Now let's explore sophisticated patterns for complex business requirements.
Time-Based Access Control
Perfect for temporary permissions or seasonal access:
// Temporary elevated access for special projectsconst specialProjectRole = Role.create({ app, id: 'special_project_lead', config: { name: 'Special Project Lead', description: 'Temporary leadership role for Q4 campaign', priority: 85, valid_from: new Date('2025-10-01'), valid_until: new Date('2025-12-31'), // Expires automatically metadata: {} },});
Conditional Permissions with Row-Level Security
Implement sophisticated access patterns:
// Writers can only edit articles in 'draft' or 'revision' statusconst editDraftArticles = Permission.createDataPermission({ app, id: 'edit_draft_articles', name: 'Edit Draft Articles', description: 'Can edit articles that are not yet published', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'update', conditions: { author_id: '$CURRENT_USER_ID', status: { $in: ['draft', 'revision'] }, created_at: { $gte: '$CURRENT_DATE - INTERVAL 30 days' } // Recent articles only }});
Permission Overrides for Exceptional Cases
Handle special circumstances gracefully:
// Grant temporary admin access to a writer for migrationwriterAccount.grantPermission({ permission: editAllArticles, valid_until: new Date('2025-03-31'), metadata: { reason: 'Database migration assistance', approved_by: 'editor_in_chief', ticket_id: 'MAINT-2025-001' }});// Revoke specific permission (emergency access control)freelancerAccount.denyPermission({ permission: createArticles, valid_until: new Date('2025-02-15'), metadata: { reason: 'Contract dispute - suspend access pending resolution', approved_by: 'legal_department' }});
Business-Specific Seed Examples 📊
E-Commerce Platform Seed
// Roles for online store managementconst storeManagerRole = Role.create({ app, id: 'store_manager', config: { name: 'Store Manager', description: 'Full store operations management', priority: 90 }});// Inventory permissionsconst manageInventory = Permission.createDataPermission({ app, id: 'manage_inventory', name: 'Manage Inventory', description: 'Full inventory control including stock levels', scope: 'table', schema_name: 'public', table_name: 'products', action: '*'});// Customer service permissionsconst viewCustomerOrders = Permission.createDataPermission({ app, id: 'view_customer_orders', name: 'View Customer Orders', description: 'Access customer order history for support', scope: 'table', schema_name: 'public', table_name: 'orders', action: 'select', conditions: { status: { $in: ['pending', 'processing', 'shipped'] } }});
Healthcare System Seed
// HIPAA-compliant role structureconst doctorRole = Role.create({ app, id: 'doctor', config: { name: 'Doctor', description: 'Licensed physician with patient care access', priority: 85, metadata: { license_required: true, hipaa_certified: true } }});// Restricted patient data accessconst viewPatientRecords = Permission.createDataPermission({ app, id: 'view_patient_records', name: 'View Patient Records', description: 'Access to assigned patient medical records', scope: 'table', schema_name: 'medical', table_name: 'patient_records', action: 'select', conditions: { // Only patients assigned to this doctor assigned_doctor_id: '$CURRENT_USER_ID', // Only active cases case_status: 'active' }});
SaaS Multi-Tenant Seed
// Tenant-isolated permissionsconst tenantAdminRole = Role.create({ app, id: 'tenant_admin', config: { name: 'Tenant Administrator', description: 'Admin access within tenant boundary', priority: 80 }});const manageTenantData = Permission.createDataPermission({ app, id: 'manage_tenant_data', name: 'Manage Tenant Data', description: 'Full access to tenant-specific data', scope: 'table', schema_name: 'public', table_name: '*', action: '*', conditions: { // Tenant isolation tenant_id: '$CURRENT_USER_TENANT_ID' }});
Generating and Deploying Your Custom Seed 🚀
Step 1: Generate Your Seed
# Generate your custom seedpnpm --filter '@kit/schema' schema:gen-seed -- --seed editorial --output out/editorial-permissions.sql
The command will:
- Execute the
schema:gen-seed
command from the@kit/schema
package. - Use the
editorial
seed template defined inpackages/schema/src/templates/editorial-seed.ts
. - Output the generated SQL to
out/editorial-permissions.sql
.
Step 2: Create Migration
In the next step, we will create a migration to apply this seed to your Supabase database.
# Create Supabase migrationsupabase migration new editorial_permissions_setup
Now copy the generated SQL file name into the newly created migration file.
# Add your generated SQLcp packages/schema/out/editorial-permissions.sql supabase/migrations/[timestamp]_editorial_permissions_setup.sql
NB: Replace [timestamp]
with the actual timestamp of your migration file. This is printed after you run the migration new
command.
Step 4: Deploy to Database
# Deploy to your Supabase projectsupabase db push# Verify deploymentsupabase migration list
Best Practices for Custom Seeds 📋
1. Planning Your Permission Structure
Start with User Stories:
As a [role], I need to [action] so that [business value]Examples:- As a Staff Writer, I need to create article drafts so that I can contribute content- As a Senior Editor, I need to publish articles so that content goes live- As a Freelancer, I need to edit my drafts so that I can refine my work
Map Business Processes:
- Who creates content?
- Who approves it?
- Who can publish it?
- What are the approval workflows?
2. Naming Conventions
Consistent Naming Patterns:
// Role naming: [Level]_[Function]'senior_editor', 'staff_writer', 'content_manager'// Permission naming: [Action]_[Resource]'create_articles', 'publish_content', 'manage_users'// Group naming: [Domain]_[Scope]'editorial_management', 'content_creation', 'user_administration'
3. Security Considerations
Principle of Least Privilege:
// ❌ Avoid: Too broad permissionsconst badPermission = Permission.createDataPermission({ name: 'Manage Everything', action: '*', table_name: '*' // Too permissive!});// ✅ Good: Specific, scoped permissionsconst goodPermission = Permission.createDataPermission({ name: 'Edit Own Articles', action: 'update', table_name: 'articles', conditions: { author_id: '$CURRENT_USER_ID' }});
Always Include Time Bounds for Elevated Access:
// Temporary elevated permissions should always expireconst tempAdminAccess = { valid_until: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days metadata: { reason: 'Emergency system maintenance' }};
Next Steps: Advanced Customization 🎯
Dynamic Permission Assignment
Create permissions that adapt to business logic:
// Example: Seasonal permissions for holiday contentconst createHolidayContent = Permission.createDataPermission({ app, id: 'create_holiday_content', name: 'Create Holiday Content', description: 'Can create holiday-themed articles during Q4', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'insert', conditions: { category: 'holiday', // Only during Q4 created_at: { $gte: '2025-10-01', $lte: '2025-12-31' } }});
Remember: A well-designed permission seed is the foundation of a secure, scalable application. Take time to model your organization's actual workflows and access patterns before implementing!