Overview
Gates are blocking resources that users must complete before accessing other content in a scope. They work at three scope levels (USER, PROGRAM, PROJECT) and support both form-based and content-only completion.
Key Concepts:
- Form gates - Require form submission (blocks with collectsData capability)
- Content gates - Require acknowledgment only (read-only blocks)
- Gate versioning - Republishing increments version, re-triggering for all users
- Gate ordering - Multiple gates in scope are shown in gateOrder sequence
Architecture
Data model and scope hierarchy
┌─────────────────────────────────────────────────────────────────┐
│ GATE FLOW │
├─────────────────────────────────────────────────────────────────┤
│ User navigates → Layout checks USER scope gates │
│ ↓ │
│ Pending gate? → Redirect to /gate/[id]?continue=... │
│ ↓ │
│ Gate interstitial page renders blocks │
│ ↓ │
│ Form gate: Submit → GateCompletion created │
│ Content gate: "I Acknowledge" → GateCompletion created │
│ ↓ │
│ Next gate or continue URL │
└─────────────────────────────────────────────────────────────────┘
FormDefinition fields:
isGate: Boolean- Is this a blocking gate?gateOrder: Int?- Order within scope (lower = first)gateVersion: Int- Increments on republish
GateCompletion model:
gateId, userId, gateVersion- Unique keyscopeType, programId, projectId- Scope contextcompletedAt- Completion timestamp
API Routes
Gate API endpoints
// GET /api/v1/gates - List pending gates
// Query params: scopeType, programId, projectId, checkAll
// GET /api/v1/gates/[id] - Get gate details for display
// POST /api/v1/gates/[id]/complete - Complete via acknowledgment
// Body: { method: 'acknowledgment', continueUrl?: string }Gate Checking
Check for pending gates
// Check for pending gates at scope entry
import { checkGatesForNavigation, checkAllScopesForGates } from '@/features/gates';
// Check specific scope
const result = await checkGatesForNavigation(userId, {
scopeType: 'PROGRAM',
programId,
basePath: `/${locale}`,
});
// Check all scopes in order: USER -> PROGRAM -> PROJECT
const result = await checkAllScopesForGates(userId, programId, projectId);
// Result structure
// {
// hasPendingGates: boolean,
// pendingGates: PendingGate[],
// nextGate: PendingGate | null,
// redirectUrl: string | null,
// }Gate Completion
Complete a gate
// Complete a gate (form submission or acknowledgment)
import { completeGate } from '@/features/gates';
// Called automatically after form submission if resource.isGate
await completeGate(userId, gateId, 'submission');
// Called from acknowledgment button for content-only gates
await completeGate(userId, gateId, 'acknowledgment');Integration Points
How gates integrate with existing systems
Layout Integration (USER scope check):
// In app layout, USER scope gates are checked on every navigation
// src/app/[locale]/(app)/layout.tsx
const gateCheck = await checkGatesForNavigation(session.user.id, {
scopeType: 'USER',
basePath: `/${locale}`,
});
if (gateCheck.hasPendingGates && gateCheck.redirectUrl) {
const continueUrl = encodeURIComponent(pathname || `/${locale}`);
redirect(`${gateCheck.redirectUrl}?continue=${continueUrl}`);
}Submission Integration (auto-complete form gates):
// In submissions route, gate completion is triggered automatically
// src/app/api/v1/resources/[id]/submissions/route.ts
if (resource.isGate) {
completeGate(auth.userId!, id, 'submission').catch((error) => {
logger.error('Failed to complete gate', {
resourceId: id,
submissionId: submission.id,
error: error instanceof Error ? error.message : 'Unknown error',
});
});
}Gate Versioning
How gate versioning works
// Gates use versioning for republish re-triggering // When a gate is republished, gateVersion increments // GateCompletion records are version-specific // @@unique([gateId, userId, gateVersion]) // User sees gate again after republish because // their completion is for the old version
Important:
Gate completions are version-specific. When a gate is republished, all users will see the gate again because their GateCompletion records reference the old version.
Feature Module
Feature module structure
src/features/gates/
├── index.ts # Public exports
├── types.ts # PendingGate, GateCheckResult, GateForInterstitial
├── schemas.ts # Zod validation (gateIdParamsSchema, etc.)
├── repository.ts # Database queries (getPendingGatesForScope, etc.)
└── services.ts # Business logic (checkGatesForNavigation, completeGate)