ASAgriSense/Docs/Data Flow & Auth

Architecture

Data Flow & Auth

Request lifecycle#

Every authenticated web request passes through four layers:

text
Browser
  → Clerk middleware (apps/web/middleware.ts)
      → Next.js App Router (edge or server)
          → Hono.js API (apps/api — Cloudflare Worker)
              → Neon PostgreSQL (Drizzle ORM)

Clerk middleware#

apps/web/middleware.ts uses Clerk's Next.js middleware to protect all routes under /app/**. Unauthenticated requests are redirected to the marketing sign-in page. The middleware runs at the edge and adds no database round-trip.

Hono API auth pattern#

The Hono API receives requests with a Clerk session token in the Authorization: Bearer header. A global middleware on the Hono app verifies the JWT using jose (edge-compatible) and extracts the following claims:

typescript
// AppEnv type in apps/api/src/types.ts
interface AuthContext {
  userId: string;      // Clerk user ID
  orgId: string;       // Clerk org ID — scopes all DB queries
  role: string;        // org:admin | org:manager | org:officer
  permissions: string[]; // dot-notation permission strings
}

// Every route handler accesses auth via Hono context:
app.get('/farmers', async (c) => {
  const { orgId, permissions } = c.get('auth');
  // all queries are scoped to orgId
});
No database hit on every request
Permissions are embedded directly into the JWT session claims by Clerk at sign-in time via session claim templates. The edge API verifies the JWT and reads permissions from the token — no Neon round-trip needed for auth on every request.

Multi-tenancy model#

Each commodity firm or cooperative maps to exactly one Clerk Organization. The organization's ID (org_...) is the tenant key. Every Neon table that stores tenant data has an org_id column. Every query is scoped to the authenticated user's orgId claim.

Clerk conceptPlatform concept
Clerk OrganizationTenant (one agribusiness entity / cooperative)
Clerk org memberUser within that tenant
Clerk org role (admin/manager/officer)Coarse role assignment
Clerk publicMetadata (on OrgMembership)Fine-grained permission set
Clerk org ID (org_...)Primary tenant key — scopes all Neon queries

Neon reference tables#

sql
-- Tenant settings (Clerk org is the identity source)
CREATE TABLE orgs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  clerk_org_id TEXT UNIQUE NOT NULL,  -- e.g. org_2abc123
  name VARCHAR(255) NOT NULL,
  slug VARCHAR(100) UNIQUE NOT NULL,
  plan VARCHAR(50) DEFAULT 'free',
  settings JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Domain data always references org_id, not clerk_org_id directly
CREATE TABLE farmers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID NOT NULL REFERENCES orgs(id),
  -- ...
);

RBAC — permission layer#

Fine-grained permissions are defined in shared/auth-permissions/src/index.ts as a PERMISSIONS const object. Three roles exist, each with a fixed permission set:

Role IDPermission set
org:adminAll permissions
org:managerAll except compliance.evidence_package, audit.report, surveys.respond, and some remediation permissions
org:officerView + create for most domains; respond to surveys; upload remediation evidence; no management of other users

Permission domains#

DomainPermissions
Settingssettings.view, settings.manage
Regionsregions.view, regions.manage
Districts/Communitiesdistricts.manage, communities.manage
Farmersfarmers.view, farmers.create, farmers.manage
Farms & Plotsfarms.view, farms.manage, plots.view, plots.manage
Programsprogram_types.view/manage, programs.view/create/manage, activities.view/create/manage, attendance.record, inputs.record, evidence.upload, coverage.view
Surveyssurveys.view/create/manage/publish/assign/respond/export
Deforestationassessments.view/request, remediation.manage, remediation.evidence_upload, risk.override
Compliancecompliance.view, compliance.report_generate, compliance.evidence_package, audit.view, audit.report

Clerk permission key format#

Clerk dashboard permission keys only allow [a-z0-9_]. Our canonical permission format uses dots (farms.view). Two helper functions in @repo/auth-permissions handle the round-trip:

typescript
import { toClerkPermissionKey, toCanonicalPermission } from '@repo/auth-permissions';

// When writing to Clerk publicMetadata:
toClerkPermissionKey('farms.view')       // → 'farms_view'

// When reading JWT claims from Clerk:
toCanonicalPermission('farms_view')      // → 'farms.view'

SWR data fetching pattern#

Frontend feature packages use SWR for all client-side data fetching. Shared configuration lives in @repo/swr-config and is applied globally via the SWR provider in apps/web/app/providers.tsx.

PatternConvention
Hook naminguseFarmers, useFarmerDetail, useBulkImport
SWR keyURL string matching the API route — e.g. /farmers?orgId=...
Error handlingSWR error state; display via ErrorBoundary or per-hook check
Mutationmutate() after POST/PATCH/DELETE to revalidate affected keys
Optimistic updatemutate() with optimisticData for immediate UI feedback