CentercodePorygon
DittoPorygonAPI

Gates

Blocking resources that users must complete before proceeding

SafeReliable

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 key
  • scopeType, programId, projectId - Scope context
  • completedAt - 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)