Skip to main content

Functional Logic

The security review proves the system can’t be exploited. This section proves it works correctly.

Form Submission Flow

The most important code path. Everything flows through submitFormResponse():
Steps 1–8 are synchronous — the submission must save before the user gets a response. Step 9 (hooks) is non-blocking. If Airtable is down or a webhook fails, the submission is already safe in the database.
Source: src/app/actions.ts, submitFormResponse() (~line 2512) Tests: 3 cases in form-renderer.test.tsx (rendering), integration coverage across encryption, rate limiting, and queue tests.
pnpm --dir apps/terra test -- -t "FormRenderer"
Try to break it:
  • Submit to a closed/unpublished form — should return a clear error, not a 500
  • Submit 31 times in a minute — rate limiting should kick in
  • Disable a webhook endpoint, submit a form — submission should still save

Notifications

Email (Resend) and SMS (Twilio) notifications fire on form events. Admin-only test sends verify provider configuration.
// Only admins can send test notifications
const result = await testEmailProvider(recipientEmail);
// Non-admin → { success: false, error: "Not authorized — admin access required" }
Tests: 37+ cases across 3 files
pnpm --dir apps/terra test -- -t "Notification"
  • Event retrieval with filtering by form, channel, status - Pagination returns distinct results - Stats math: sent count = (sent + delivered), excludes failed/pending - Error categorization: invalid address, bounced, carrier rejected, rate limited, provider error - Admin-only enforcement on test email/SMS - Missing provider config caught (RESEND_API_KEY, Twilio credentials) - Per-form notification settings: auth enforced, insert vs update path, null handling

Async Queue

Post-submission hooks use an async queue with exponential backoff:
// src/lib/async-queue.ts
// Backoff formula: 2^(attempts-1) * 30 seconds

// Webhook:    30s → 60s → 120s → 240s → 480s  (5 attempts)
// Airtable:   30s → 60s → 120s                  (3 attempts)
// Plaid:      30s → 60s → 120s                  (3 attempts)
// Email:      30s → 60s → 120s                  (3 attempts)
Queue operations are resilient — errors return null instead of throwing:
const result = await enqueueWebhook(payload);
if (!result) {
  logger.error("Failed to enqueue webhook"); // Logged, not thrown
}
Tests: 20+ cases in src/lib/__tests__/async-queue.test.ts
pnpm --dir apps/terra test -- -t "Async Queue"
  • Correct RPC payload structure for enqueue - Webhook gets 5 retries, others get 3 - Queue errors return null (no crash) - Database timeout and connection reset handled gracefully - Complex nested payloads preserved (unicode, special chars, nested objects) - Backoff formula verified: 2^(n-1) * 30s

Airtable Sync

Bidirectional sync with loop prevention. System fields use _ prefix convention:
// Field mapping conventions
const SYSTEM_FIELDS = ["_submission_id", "_status", "_payment_status"];
const FORM_FIELDS = ["full_name", "email"]; // No prefix

// Loop prevention: airtable_sync source blocks reverse trigger
const CHANGE_SOURCES = ["manual", "airtable_sync", "webhook", "system"];
Safety constraint: the Airtable client exposes create, update, and upsertno delete operation. Tests: 15 cases in airtable-status-sync.test.ts
pnpm --dir apps/terra test -- -t "Airtable Status Sync"
  • Missing/disabled connections skip gracefully - Unmapped status fields skip (no crash) - Only valid statuses accepted (submitted, pending, processing, approved, rejected, withdrawn) - _submission_id mapping required to find Airtable record - No delete operation exposed — records preserved on withdrawal - Sync loop prevention: airtable_sync source blocks reverse trigger - Change source tracked for audit

Import Security

Forms can be imported from PDFs, images, and HTML. The import pipeline prevents user impersonation:
// User ID always comes from the session, never from the request
const jobId = `import_${session.user.id}_${timestamp}`;

// Even if an attacker passes a userId parameter, it's ignored
startImageImportJob({ userId: "attacker-999" });
// → Uses session.user.id, not "attacker-999"
Tests: 10 cases in import-security.test.ts
pnpm --dir apps/terra test -- -t "Import Security"
  • Job ID contains authenticated user’s ID, not attacker’s - Unauthenticated users rejected - userId parameter in request body ignored - Separate jobs for different users (no cross-contamination) - Max 8 images, max 10MB PDF, only PDF file type accepted