Croftura
in progressCroftura is a multi-tenant garden and farm management platform — a web application where homesteads can track plantings, manage members, and build a shared plant encyclopedia. It runs in UAT at app.uat.croftura.com, provisioned entirely from Bicep IaC across a purpose-built Azure resource group.
What It Is
A role-aware SaaS product for small farms and homesteads. Each organisation (tenant) gets its own isolated workspace where owners invite workers and viewers, members track what they grow, and everything is backed by a shared global plant encyclopedia that anyone can contribute to.
The primary design constraint: authentication and multi-tenancy must require zero infrastructure per new customer. A new organisation is created end-to-end by the user — no developer involvement, no database seeding, no Entra portal work after the initial setup.
Architecture
Croftura is split into four tiers: two React SPAs (customer and admin), a FastAPI backend running in a Container App, and a PostgreSQL database. All Azure resources are defined in a single Bicep template deployed to croftura-uat.
Browser (React 19 + MSAL.js v3)
│
│ PKCE auth flow
▼
Microsoft Entra External ID (CIAM)
│
│ JWT with groups[] claim
▼
FastAPI (Azure Container App)
│ Validates JWT (iss, aud, exp)
│ Resolves role: groups[] → tenant_groups table → (tenant_id, role)
│
├── PostgreSQL Flexible Server — tenants, plants, invites, audit_log
├── Azure Blob Storage — observation photos
├── Azure Key Vault — DATABASE_URL (managed identity, no secrets in env)
└── Microsoft Graph API — group + user management for org provisioning
Multi-Tenancy Model
Tenant membership lives entirely in Entra, not in a custom users table. Each organisation maps to three Entra security groups — owner, worker, viewer — named with the tenant UUID. When a user signs in, Entra includes their group memberships in the JWT groups claim. The API resolves the role on every request by joining those group IDs against the tenant_groups table.
JWT groups claim: ["group-obj-id-1", "group-obj-id-2"]
│
tenant_groups table
(group_id, tenant_id, role)
│
→ role = "owner" for tenant X
The client sends an X-Tenant-ID header on all tenant-scoped requests. A GET /api/v1/me call (no tenant header required) returns every tenant the signed-in user belongs to along with their role in each — used by the frontend to present a tenant selector at login.
Self-Service Onboarding
New users who sign in with no group memberships land in an Onboarding Wizard. They can either create a new organisation or join one with an invite code. Both paths are fully automated:
Create — a five-step saga: verify no existing membership → generate tenant UUID → create three Entra security groups via Graph API → add user to owner group → commit the tenant and tenant_groups rows in a single DB transaction. If any post-group-creation step fails, the saga deletes the created groups before returning a 503.
Join — invite codes support configurable expiry and use limits. The join endpoint uses SELECT FOR UPDATE on the invite row to prevent race conditions on limited-use codes, then adds the user to the relevant Entra group via Graph before committing.
After either path, the user is redirected through a full MSAL sign-out and back in, which mints a fresh JWT containing the new group membership. No polling, no manual token refresh.
Plant Encyclopedia
A global plant database backed by PostgreSQL, with full-text and fuzzy search powered by the pg_trgm extension. Search results merge global plants with the caller's homestead-scoped custom plants, ranked by trigram similarity against common and scientific name.
Custom plants are tenant-isolated by a database constraint:
-- scope='homestead' requires tenant_id; scope='global' requires tenant_id IS NULL
ALTER TABLE plants ADD CONSTRAINT plants_scope_tenant_check CHECK (
(scope = 'homestead' AND tenant_id IS NOT NULL)
OR (scope = 'global' AND tenant_id IS NULL)
);
Members can flag their custom plants for admin review. Platform admins can then import flagged plants into the global encyclopedia, where they land as unpublished drafts for editorial review before going live.
Stack
- React 19 — customer SPA and admin SPA, built with Vite
- MSAL.js v3 — PKCE-based authentication against Entra External ID; no implicit flow
- FastAPI (Python 3.12) — API tier; containerised and deployed to Azure Container Apps
- PostgreSQL Flexible Server — primary data store; Alembic for schema migrations
- Microsoft Entra External ID (CIAM) — authentication and tenant group membership
- Microsoft Graph API — automated security group provisioning during org onboarding
- Azure Container Registry — stores and serves the API Docker image
- Azure Key Vault — database connection string; accessed by the Container App via managed identity, no secrets in environment variables
- Azure Blob Storage — observation photo uploads
- Azure Static Web Apps (×2) — customer and admin frontends with custom domain binding
- Azure Bicep — all infrastructure defined as code; single template, per-environment parameter files
UAT Environment
The UAT environment is live at:
- Customer app:
app.uat.croftura.com - Admin panel:
admin.uat.croftura.com - API:
api.uat.croftura.com(TLS via Container App managed certificate)
Infrastructure is deployed to the croftura-uat resource group. Deployments are currently manual via az deployment group create + az acr build; the GitHub Actions workflow automates the build, migration, and frontend deploy steps once credentials are wired up.
Status
UAT infrastructure is provisioned and live. Authentication, multi-tenancy, self-service onboarding, invite codes, the plant encyclopedia, and custom plant workflows are all deployed and functional. Current focus: expanding the core tracking features — plantings, harvest logs, and observation records — and completing CI/CD pipeline configuration for automated UAT deployments.