Architecture Overview
System overview
Section titled “System overview”Miranda is an API-first platform with two consumer surfaces:
- Foundation Dashboard — a Vue 3 SPA for foundation administrators (served at
app.miranda.foundation) - Donor Portal — a separate Vue 3 SPA for donors (served at
miranda.gives)
Both SPAs communicate with a single Laravel 12 API backend. Public-facing pages (donation forms, scholarship applications) are server-rendered Blade templates.
┌─────────────────────┐ ┌─────────────────────┐│ Foundation SPA │ │ Donor Portal SPA ││ Vue 3 + PrimeVue │ │ Vue 3 ││ app.miranda.fdn │ │ miranda.gives │└────────┬────────────┘ └────────┬────────────┘ │ Bearer JWT │ Bearer JWT │ X-Foundation-Id │ (donor role) ▼ ▼┌─────────────────────────────────────────────────┐│ Laravel 12 API ││ ┌──────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ││ │Foundation │ │ Donor │ │Public │ │Webhooks│ ││ │Endpoints │ │Endpts │ │Endpts │ │(Stripe)│ ││ └─────┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ ││ └──────────┴──────────┴───────────┘ ││ Services Layer │└──────────┬──────────────────┬───────────────────┘ │ │ ┌──────▼──────┐ ┌──────▼──────┐ │ PostgreSQL │ │ Stripe │ │ (Supabase) │ │ Connect │ └─────────────┘ └─────────────┘Multi-tenancy
Section titled “Multi-tenancy”Miranda uses a shared database model. Every tenant-scoped table includes a foundation_id foreign key.
- Tenant model:
Foundation— represents a nonprofit organization on the platform - Scoping: All queries in controllers filter by
$request->user()->foundation_id - Multi-foundation users: A single Supabase auth user can have
Userrecords in multiple foundations. TheX-Foundation-Idrequest header selects the active context.
Request lifecycle
Section titled “Request lifecycle”Request → SupabaseAuth middleware (decode JWT, resolve User by auth_user_id + X-Foundation-Id) → EnsureFoundationContext middleware (verify user has foundation_id) → Controller (all queries scoped to foundation_id)Authentication
Section titled “Authentication”Miranda uses a dual-layer auth system:
Layer 1: Supabase Auth (identity)
Section titled “Layer 1: Supabase Auth (identity)”Supabase handles registration, login, password reset, and JWT issuance. The SupabaseAuthService decodes tokens using:
- JWKS (production) — fetches public keys from
{SUPABASE_URL}/auth/v1/.well-known/jwks.json, cached for 1 hour - HMAC secret (testing) — falls back to
SUPABASE_JWT_SECRETfor HS256 tokens
// SupabaseAuthService key methods$decoded = $authService->decodeToken($jwt); // Returns claims object$userId = $authService->getAuthUserId($decoded); // Supabase UUID (sub claim)$email = $authService->getEmail($decoded); // Email from token$roles = $authService->getRoles($decoded); // app_metadata.roles arrayLayer 2: Miranda User records (authorization)
Section titled “Layer 2: Miranda User records (authorization)”Each Supabase auth user maps to one or more User records in Miranda’s database. The User model holds foundation_id, role (legacy UserRole enum), and role_id (new Role model FK).
Middleware stack
Section titled “Middleware stack”| Middleware | Alias | Purpose |
|---|---|---|
SupabaseAuth | auth.supabase | Decodes JWT, resolves User. strict (default) requires existing User; relaxed allows registration |
EnsureFoundationContext | foundation | Rejects requests where user has no foundation_id |
EnsureRole | role:{roles} | Checks app_metadata.roles from the Supabase token |
Route groups
Section titled “Route groups”| Group | Middleware | Purpose |
|---|---|---|
public/* | None | Consumed by miranda.gives and public pages |
donor/* | auth.supabase:relaxed, role:donor | Donor portal endpoints |
/register | auth.supabase:relaxed | Foundation registration (no User record yet) |
| Foundation routes | auth.supabase, foundation | All admin operations |
Services layer
Section titled “Services layer”Business logic is extracted into dedicated service classes. Controllers remain thin.
| Service | Responsibility |
|---|---|
DonationService | Payment processing, contact creation, fee calculation, donor stats |
StripeConnectService | Express account lifecycle, onboarding links, fee rate lookup |
CampaignService | Audience resolution via SegmentationService, batch job dispatch |
SequenceService | Contact enrollment, step scheduling, due-step processing |
EmailService | Resend API integration, merge field replacement, communication logging |
SmsService | Twilio integration, opt-in enforcement, communication logging |
ImportService | CSV parsing, column mapping, deduplication, Stripe import |
SegmentationService | Dynamic query builder for contact filtering |
ActivityService | Timeline event recording on contacts |
ScholarshipService | Application intake, stage transitions, award disbursement |
RolePermissionService | Permission checks against Role model and legacy UserRole |
SupabaseAuthService | JWT decode/verify, claim extraction |
Database conventions
Section titled “Database conventions”- Engine: PostgreSQL (Supabase-hosted)
- Primary keys: UUIDs on all tables via
HasUuidtrait - Amounts: Stored in cents (integer) —
$12.50is stored as1250 - JSON columns: Used for flexible data (
settings,metadata,communication_preferences,brand_colors,form_fields,responses,segment_criteria) - Migrations: All schema changes via Laravel migrations; every migration must be idempotent
Frontend architecture
Section titled “Frontend architecture”Foundation Dashboard
Section titled “Foundation Dashboard”| Layer | Technology |
|---|---|
| Framework | Vue 3.5 (Composition API) |
| UI | PrimeVue 4.5 |
| Styling | Tailwind CSS 4 |
| State | Pinia 3 |
| Routing | Vue Router 4 |
| Auth | @supabase/supabase-js |
| HTTP | Axios |
| Build | Vite 8 |
The SPA is served by a catch-all route at /app/{any} and communicates exclusively with /api/* endpoints.
Donor Portal
Section titled “Donor Portal”A separate Vue 3 SPA at miranda.gives consuming /api/donor/* endpoints. Donors authenticate via Supabase with the donor role in their JWT app_metadata.roles.
Public pages
Section titled “Public pages”Donation pages and scholarship forms are server-rendered Blade templates for SEO and fast initial load. Card data is handled client-side via Stripe.js (PCI SAQ-A compliance).
Payment architecture
Section titled “Payment architecture”Miranda uses Stripe Connect Express with destination charges:
- Each foundation onboards as a Stripe Express connected account
- Donations create
PaymentIntentobjects on the platform account withtransfer_data.destinationrouting funds to the foundation application_fee_amountdeducts Miranda’s transaction fee (2% / 1.5% / 1% by plan tier)- Donors can optionally cover Stripe processing fees + platform fees
Miranda is the merchant of record. Foundations receive funds directly into their connected Stripe accounts.
Infrastructure
Section titled “Infrastructure”| Component | Technology |
|---|---|
| Hosting | Laravel Forge on Linode |
| Server | Ubuntu, PHP 8.4, Nginx |
| Database | Supabase PostgreSQL |
| File storage | Supabase S3 |
| Resend | |
| SMS | Twilio |
| Payments | Stripe Connect Express |
| DNS/CDN | Cloudflare |
| Deploy | Git push triggers Forge Quick Deploy |
Queued jobs
Section titled “Queued jobs”Long-running operations are dispatched to the Laravel queue:
| Job | Trigger |
|---|---|
SendCampaignBatch | Campaign send (batches of 50 contacts) |
SendDonationReceipt | Successful donation with contact email |
ProcessSequenceStep | Sequence enrollment reaches next step time |
ImportContactsBatch | Large CSV import processing |
EnrichCharityDataJob | Foundation data enrichment |