Authorization Architecture
Purpose
This document defines the authorization strategy for the Farmer1st platform, managing complex relationships between users, entities, and data access across a multi-tenant ecosystem.
Current State
Authorization Model
┌─────────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION vs AUTHORIZATION │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ SUPERTOKENS │ │ OPENFGA │ │
│ │ (Authentication) │ │ (Authorization) │ │
│ │ │ │ │ │
│ │ "Who are you?" │ │ "What can you access?" │ │
│ │ │ │ │ │
│ │ • Login / Logout │ │ • Relationship-based access │ │
│ │ • Session management │ │ • Role permissions │ │
│ │ • JWT issuance │ │ • Entity hierarchies │ │
│ │ • Identity verification │ │ • Data ownership │ │
│ │ │ │ │ │
│ └─────────────────────────────┘ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Technology Choice
| Component | Technology | Deployment |
|---|---|---|
| Authorization Engine | OpenFGA | Self-managed on EKS |
| Tuple Storage | PostgreSQL | Dedicated instance (not shared with app DB) |
Decisions and Rationale
| Decision | Rationale |
|---|---|
| OpenFGA for authorization | Designed for complex relationship-based access control (ReBAC), handles graph traversals, proven at scale (Okta/Auth0) |
| Self-managed | Consistent with SuperTokens/Unleash pattern, cost control at scale |
| Separate from SuperTokens | Clear separation of concerns: authn vs authz |
| Dedicated PostgreSQL | Isolate authz workload, independent scaling, no contention with app data |
Relationship Model
The Challenge
Farmer1st has complex, multi-layered relationships:
┌─────────────────────────────────────────────────────────────────────────────┐
│ RELATIONSHIP COMPLEXITY │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ USER TYPES ENTITIES RELATIONSHIPS │
│ ────────── ──────── ───────────── │
│ • Farmer • Farm • User owns Entity │
│ • Coop Member • Family • User is member of Entity │
│ • Coop Admin • Cooperative • Entity relates to Entity │
│ • Agent • Factory • User has role on Entity │
│ • Brand Employee • Brand • Access via chain │
│ • Factory Manager • (more TBD...) • Direct or indirect │
│ • (more TBD...) │
│ │
│ EXAMPLE ACCESS PATHS │
│ ──────────────────── │
│ │
│ Farmer ──owns──► Farm │
│ │
│ Farmer ──member_of──► Cooperative ──supplies──► Nestlé │
│ │
│ Nestlé Employee ──works_for──► Nestlé ──sources_from──► Coop ──► Farm │
│ │
│ Agent ──manages──► [Farm1, Farm2, Farm3...] │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
OpenFGA Authorization Model
# Conceptual OpenFGA model (DSL)
type user
type farm
relations
define owner: [user]
define manager: [user]
define viewer: [user, cooperative#member, brand#employee]
define can_view: owner or manager or viewer
define can_edit: owner or manager
type family
relations
define head: [user]
define member: [user]
type cooperative
relations
define admin: [user]
define member: [user]
define viewer: [user, brand#employee]
define can_view: admin or member or viewer
define can_edit: admin
type brand
relations
define admin: [user]
define employee: [user]
define sources_from: [cooperative, farm]
define can_view_supplier: admin or employee
type factory
relations
define manager: [user]
define worker: [user]
define owned_by: [brand]
Access Check Examples
| Question | OpenFGA Check |
|---|---|
| Can user X view farm Y? | Check(user:X, can_view, farm:Y) |
| Can user X edit coop Z? | Check(user:X, can_edit, cooperative:Z) |
| What farms can user X view? | ListObjects(user:X, can_view, farm) |
| Who can view farm Y? | ListUsers(farm:Y, can_view) |
Indirect Access (Graph Traversal)
OpenFGA handles transitive relationships:
# Nestlé employee accessing farm data through coop relationship
Tuples:
user:alice │ employee │ brand:nestle
brand:nestle│ sources_from│ cooperative:coop1
cooperative:coop1 │ member │ user:farmer_bob
user:farmer_bob │ owner │ farm:farm123
Query: Can user:alice view farm:farm123?
↓
OpenFGA traverses: alice → nestle → coop1 → farm123
↓
Result: ✅ Yes (via brand employee → sources_from → cooperative → farm)
Integration Pattern
Authorization Check Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ AUTHORIZATION FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Request arrives (JWT validated by Cloudflare) │
│ │ │
│ ▼ │
│ 2. API extracts user ID from JWT │
│ │ │
│ ▼ │
│ 3. API calls OpenFGA: Check(user:X, action, resource:Y) │
│ │ │
│ ├── Denied → 403 Forbidden │
│ │ │
│ └── Allowed → Continue to business logic │
│ │ │
│ ▼ │
│ 4. Execute operation, return response │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Tuple Management
Tuples (relationships) are created/deleted when:
| Event | Tuple Action |
|---|---|
| Farmer registers and creates farm | user:X, owner, farm:Y |
| Farmer joins cooperative | cooperative:Z, member, user:X |
| Coop signs contract with brand | brand:A, sources_from, cooperative:Z |
| Employee added to brand | user:B, employee, brand:A |
| User removed from coop | Delete cooperative:Z, member, user:X |
Tuple changes are event-driven — published to Kafka, processed by workers that update OpenFGA.
Scaling Considerations
Expected Scale
| Metric | Estimate | Notes |
|---|---|---|
| Users | 100M | Mostly farmers |
| Relationships per user | 5-10 avg | Farm, family, coop memberships |
| Total tuples | 500M - 1B | Users + entity-to-entity relationships |
| Authz checks/sec | TBD | Most API calls require a check |
Infrastructure Planning
| Component | Recommendation |
|---|---|
| OpenFGA instances | Multiple replicas on EKS |
| PostgreSQL (tuples) | Dedicated RDS instance, not shared with app |
| Read replicas | Consider for high-read authorization patterns |
| Caching | Redis for frequent authz checks (short TTL) |
| Latency target | <10ms for authorization checks |
Performance Optimizations
- Batch checks: Use
BatchCheckfor multiple resources - List objects: Use
ListObjectsinstead of N individual checks - Caching: Cache positive authz results briefly (30s-60s)
- Denormalization: For very hot paths, consider denormalizing permissions
Trade-offs Considered
OpenFGA vs OPA (Open Policy Agent)
| Factor | OpenFGA | OPA |
|---|---|---|
| Model | Relationship-based (ReBAC) | Policy-based (Rego) |
| Graph queries | ✅ Native | ❌ Must implement |
| Complex hierarchies | ✅ Designed for it | ⚠️ Possible but harder |
| Learning curve | ✅ Simpler DSL | ❌ Rego is complex |
| Use case fit | ✅ "Who can access what" | Better for "what rules apply" |
Decision: OpenFGA is purpose-built for our relationship-heavy model.
OpenFGA vs Custom Solution
| Factor | OpenFGA | Custom |
|---|---|---|
| Development time | ✅ Ready to use | ❌ Months of work |
| Graph traversal | ✅ Built-in | ❌ Must build |
| Proven at scale | ✅ Okta/Auth0 | ❌ Unproven |
| Flexibility | ⚠️ DSL constraints | ✅ Anything goes |
Decision: Don't reinvent the wheel.
Open Questions
- Exact entity types and relationships (see
09-domain-model.md) - Caching strategy for authz checks (Redis TTL?)
- Audit logging for authorization decisions?
- How to handle relationship changes (eventual consistency?)
- OpenFGA version and upgrade strategy?
Dependencies
- SuperTokens for authentication (provides user ID)
- Kafka for event-driven tuple updates
- PostgreSQL (dedicated instance) for tuple storage
- Redis for authz caching (optional optimization)
Last Updated: 2025-12-25