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:
json
{
  "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:

yaml
// 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:

dart
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:

EventDescription
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:

ReasonError 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:

dart
// 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.