Permissions System Architecture
Answer to the Question: Permissions and Subscriptions in the Same Database
Analysis
For a medium-sized SaaS application, keeping permissions and subscriptions in the same database is generally a good solution for the following reasons:
✅ Advantages
- Simplicity: Single database to manage, backup, and monitor
- ACID Transactions: Operations can be atomic (e.g., assign a plan and check permissions in a single transaction)
- Join Queries: Easy to join user data with their permissions and subscriptions
- Consistency: No synchronization issues between databases
- Performance: No network latency between databases
- Maintenance: Simpler to maintain and debug
⚠️ Potential Disadvantages
- Coupling: Billing data is coupled with application data
- Scalability: If billing becomes very large, might require separation
- Security: All data in a single database (but can be mitigated with PostgreSQL schemas)
Recommendation
Keep in the same database for now because:
- The application is still growing
- The complexity added by separation is not justified
- We can always migrate to separation later if necessary
When to Separate?
Consider separation if:
- Billing data volume becomes very large (> millions of transactions)
- Need to isolate billing security (PCI-DSS compliance, etc.)
- Different teams manage billing vs application
- Need independent scalability
Future Alternatives
If separation becomes necessary later:
- Microservices: Separate billing service with its own database
- Dedicated Database: Separate PostgreSQL database for billing only
- Event-driven: Synchronization via events (more complex)
Users Table Cleanup
Changes Made
- Removed
rolefield: Replaced byADMIN_ACCESSpermission - Automatic migration: Existing ADMIN users are automatically migrated to ENTERPRISE plan with ADMIN_ACCESS permission
- Updated checks: All
role !== "ADMIN"checks are replaced by permission checks
Migration
The migration 0032_remove_user_role:
- Assigns
ADMIN_ACCESSpermission to all plans - Creates ENTERPRISE subscriptions for existing ADMIN users
- Removes the
rolefield from theuserstable
Usage
Before:
if (req.user.role !== "ADMIN") {
throw new ForbiddenException("Admin access required");
}
After:
@UseGuards(AuthGuard('jwt'), ActiveUserGuard, PermissionGuard)
@RequirePermission('ADMIN_ACCESS')
async adminEndpoint() {
// ...
}
Or in a service:
await verifyAdmin(this.permissionsService, userId, this.logger, 'actionName');
Current Structure
Main Tables
users: Basic information (withoutrole)user_subscriptions: Active user subscriptionssubscription_plans: Available plans (FREE, BASIC, PRO, ENTERPRISE)permissions: Available granular permissionsplan_permissions: Plan-permission associationsplan_limits: Numeric limits per plan
Verification Flow
- User makes a request
AuthGuard('jwt')validates the tokenActiveUserGuardchecks ACTIVE statusPermissionGuardchecks required permission (if decorator present)PermissionsServiceloads permissions from database- Verification performed and response returned
Performance
- Permission checks: 1 SQL query per check (can be optimized with cache)
- Limit checks: 1 SQL query + 1 count query
- Scalability: Indexes on
user_subscriptionsandplan_permissionsensure good performance
Scalability
The system is designed to be scalable:
- Easy addition of new permissions
- Easy addition of new plans
- Limit modifications without complex migration
- Possibility to add Redis cache later if necessary