Preview Registration Feature Flags
This document describes the early access registration gating system using feature flags. This feature allows controlled rollout of user registration during preview and beta phases.
Overview
The preview registration system uses feature flags stored in the feature_flags table to control access. Feature flags are global and evaluated by name only.
Required Feature Flags
preview_invite_only
Controls whether registration is restricted to invite-only mode.
- Type: boolean (enabled column)
- Behavior:
- enabled=true => /api/auth/register returns 403 Forbidden
- enabled=false => registration proceeds to next validation rule
preview_registration_enabled
Controls general registration availability with user limit caps.
- Type: boolean (enabled column) + JSONB payload (value)
- Required JSONB payload:
{
"target_users": 100
}
- Behavior:
- enabled=false => /api/auth/register returns 403 Forbidden
- enabled=true => registration allowed until user count reaches target_users
- When current_users >= target_users => registration blocked with 403 Forbidden
Registration Decision Flow
The backend evaluates registration requests in the following order:
// Simplified decision logic from RegistrationDecision class
if (previewInviteOnlyEnabled) {
return RegistrationDecision.block(
reason: 'preview_invite_only_enabled',
);
}
if (!previewRegistrationEnabled) {
return RegistrationDecision.block(
reason: 'preview_registration_disabled',
);
}
final targetUsers = extractTargetUsers(registrationFlag.value);
if (targetUsers == null || targetUsers <= 0) {
return RegistrationDecision.block(
reason: 'preview_registration_target_missing',
);
}
final currentUsers = await DatabaseManagers.users.count();
if (currentUsers >= targetUsers) {
return RegistrationDecision.block(
reason: 'preview_registration_limit_reached',
details: {
'current_users': currentUsers,
'target_users': targetUsers,
},
);
}
return RegistrationDecision.allow(
reason: 'preview_registration_open',
details: {
'current_users': currentUsers,
'target_users': targetUsers,
},
);
RegistrationDecision Model
The backend uses a RegistrationDecision class to encapsulate registration validation results:
class RegistrationDecision {
final bool allowed;
final String reason;
final Map<String, dynamic> details;
RegistrationDecision._({
required this.allowed,
required this.reason,
this.details = const {},
});
factory RegistrationDecision.allow({
required String reason,
Map<String, dynamic> details = const {},
}) {
return RegistrationDecision._(
allowed: true,
reason: reason,
details: details,
);
}
factory RegistrationDecision.block({
required String reason,
Map<String, dynamic> details = const {},
}) {
return RegistrationDecision._(
allowed: false,
reason: reason,
details: details,
);
}
}
Decision Order Summary
- Invite-only check: If preview_invite_only.enabled == true => block registration
- Registration enabled check: If preview_registration_enabled.enabled != true => block registration
- Target users validation:
- Read preview_registration_enabled.value.target_users
- Missing/invalid target => block registration
- current_users >= target_users => block registration
- Otherwise => allow registration
Audit Logging
All registration decisions are logged to the logs table for monitoring and debugging:
| Event | Description |
|---|---|
| auth_register_blocked | Registration was blocked with reason and details |
| auth_register_allowed | Registration was allowed with current/target user counts |
Each log entry includes context such as:
- Requesting email
- Decision reason
- Current/target user counts (when applicable)
Error Messages
When registration is blocked, users receive appropriate error messages:
| Reason | Error Message |
|---|---|
| preview_invite_only_enabled | "Registration is currently invite-only." |
| preview_registration_disabled | "Registration is currently unavailable for preview." |
| preview_registration_target_missing | "Registration is currently unavailable for preview." |
| preview_registration_limit_reached | "Preview registration limit has been reached." |
Setup Example
To enable preview registration with a 100-user limit:
// Insert invite-only flag (initially disabled)
await DatabaseManagers.featureFlags.insert({
'name': 'preview_invite_only',
'enabled': false,
});
// Insert registration enabled flag with target
await DatabaseManagers.featureFlags.insert({
'name': 'preview_registration_enabled',
'enabled': true,
'value': {'target_users': 100},
});
Deferred Scope
The following features are planned for future implementation:
- Campaign import and management
- Invite cohort management
- Campaign-level audit tables
These are tracked as follow-up tasks.