Skip to content
Owden Magnusen, home

Anchor by Aurelo

Event Infrastructure Operating System.

Problem

Live events in Africa are run on a patchwork of tools. A church or conference might sell tickets on one platform, stream on another, collect donations through a third, and lose the recording afterward. Each tool bills in foreign currency, assumes card payments, and knows nothing about the others. Creators and broadcasters face the same split between ticketing, streaming and archiving.

Anchor by Aurelo is a multi-tenant SaaS platform built as an Event Infrastructure Operating System: live streaming, ticketing, pay-per-view and event management in one product. The target customers are faith-based organizations, creators, conferences and broadcasters, primarily across Africa. The core design problem is giving each organization its own isolated, branded workspace while running everything on one platform that a very small team can operate.

Constraints

  • Team size. Five people in total. One engineer owns the backend, infrastructure, org console, viewer app and both mobile apps. Every architectural choice has to be maintainable by one person.
  • Local payments. Ghana mobile money is a first-class payment path, not an add-on. Ticket checkout needs a server-initiated charge that avoids browser redirects.
  • Cost. Video storage and egress dominate the bill for a streaming product. Archived recordings need a cheap tier, and delivery of archived assets should avoid egress fees.
  • Isolation. Organizations must not see each other's data, and a tenant-level mistake must not become a platform-level incident.
  • Pre-launch. The platform is being tested with a small group of organizations ahead of its commercial launch, so there is room to fix foundations, but no staging environment yet.
  • Contract-first delivery. No frontend screen is built until a Figma frame, an API contract, a permission model and a component state model are agreed. This keeps a one-person full-stack effort from drifting.

Service boundaries

Anchor is a modular monolith, not a set of microservices. The boundaries are enforced inside one FastAPI codebase and one deployable, with asynchronous work pushed to workers over a message bus. For a small team this avoids distributed-system overhead while keeping modules separable later.

Anchor architectureThe Platform Console, Viewer Web App, and Mobile Apps reach Cloudflare, then an OCI load balancer that terminates TLS, then the Anchor API: one FastAPI modular monolith with Identity and Access, Tenancy, Events and Ticketing, Pay-Per-View, Streaming and Media, Payments and Payouts, Notifications, Platform Admin, and Mobile Config modules sharing a service layer. The service layer uses PostgreSQL with a schema per tenant, Redis for tokens, OTPs, and presence, and RabbitMQ, which feeds a pool of background workers. External services are Mux, Paystack, Resend and mNotify, Backblaze B2, and Cloudinary.ANCHOR API, FASTAPI MODULAR MONOLITHEXTERNAL SERVICESHTTPSpublishconsumePlatform ConsoleNext.js, admin and workspaceViewer Web AppNext.jsMobile AppsReact Native, iOS and AndroidCloudflareDNS and edgeLoad BalancerOCI, TLS terminationTenancyschema routingIdentity and Accesstokens, OTPs, RBACPlatform Adminstaff ops, audit logEvents and Ticketingevents, ordersPay-Per-Viewpaid stream accessMobile Configremote configStreaming and MediaMux, replaysPayments and PayoutsPaystackNotificationsemail and SMSService layerin-process module callsPostgreSQLschema per tenantRedistokens, OTPs, presenceRabbitMQevent busWorkersasync jobsMuxvideo, streamingPaystackpaymentsResend, mNotifyemail and SMSBackblaze B2exports, archiveCloudinarymedia assets
Anchor architectureprimary path
Diagram as text
  • Platform Console to Cloudflare
  • Viewer Web App to Cloudflare
  • Mobile Apps to Cloudflare
  • Cloudflare to Load Balancer (HTTPS)
  • Load Balancer to Identity and Access
  • Service layer to PostgreSQL
  • Service layer to Redis
  • Service layer to RabbitMQ (publish)
  • RabbitMQ to Workers (consume)
  • Workers to PostgreSQL
  • Tenancy (schema routing)
  • Platform Admin (staff ops, audit log)
  • Events and Ticketing (events, orders)
  • Pay-Per-View (paid stream access)
  • Mobile Config (remote config)
  • Streaming and Media (Mux, replays)
  • Payments and Payouts (Paystack)
  • Notifications (email and SMS)
  • Mux (video, streaming)
  • Paystack (payments)
  • Resend, mNotify (email and SMS)
  • Backblaze B2 (exports, archive)
  • Cloudinary (media assets)

Clients

  • Platform Console (Next.js): one app hosting Platform Admin and the Organization Workspace behind a shared login
  • Viewer Web App (Next.js)
  • Mobile Apps (React Native, iOS and Android)
  • Marketing Site

Edge

  • Cloudflare (DNS and edge)
  • Load Balancer (OCI flexible load balancer, TLS termination)

Anchor API (FastAPI modular monolith), logical modules

  • Identity and Access: login, tokens, OTPs, RBAC
  • Tenancy: organization provisioning and schema routing
  • Events and Ticketing: events, ticket types, orders
  • Pay-Per-View: viewer access to paid streams
  • Streaming and Media: Mux integration, stream lifecycle, replays
  • Payments and Payouts: Paystack checkout, subscription billing, organizer payouts
  • Notifications: email and SMS dispatch
  • Platform Admin: staff operations and audit logging
  • Mobile Config: remote configuration for the mobile apps

Workers

  • A pool of worker containers consuming from the message bus for webhooks, notifications, payouts and media tasks

Data and messaging

  • PostgreSQL (schema-per-tenant)
  • Redis (refresh tokens, OTPs, presence)
  • RabbitMQ (a topic exchange for domain events)

External services

  • Mux (live streaming and video)
  • Paystack (payments and transfers)
  • Resend and mNotify (email and SMS)
  • Backblaze B2 (exports and archive)
  • Cloudinary (images and media assets)

Modules call each other in-process through a service layer. Anything slow or retry-prone, such as webhook handling, notifications and payout state changes, goes onto the message bus and is handled by workers.

Multi-tenancy model

Anchor uses schema-per-tenant PostgreSQL. Each organization gets its own schema. Two shared schemas sit alongside them: one for staff-level platform data and one for shared reference data.

Why schema-per-tenant rather than the alternatives:

  • Versus shared tables with a tenant column. Isolation is structural. A query that forgets a tenant filter cannot leak another tenant's rows because the tables are physically separate.
  • Versus database-per-tenant. It avoids the connection and operational cost of hundreds of databases while still giving per-tenant backup, restore and export boundaries.

Requests resolve the tenant from the authenticated token and set the Postgres search_path to that organization's schema, so the same service code runs against whichever tenant is calling. One project convention supports this: fixed-shape data uses flat, typed columns, not JSONB. That keeps schemas predictable across tenants and makes migrations mechanical.

The trade-off is migration fan-out. Every schema change must be applied to every tenant schema, and connection pooling has to respect per-request search_path. Both are covered in the last section.

Auth model

Anchor issues three distinct JWT shapes, one per kind of actor:

  • Tenant tokens for members of an organization using the Organization Workspace. They carry the organization context used to select the schema, and permissions are enforced through role-based access control.
  • Platform tokens for Aurelo staff using Platform Admin. These work against the shared platform schema, and sensitive actions are audit logged.
  • Viewer tokens for audience members watching events and buying tickets. They carry viewer identity, not organization membership.

Keeping the shapes separate means a viewer token can never be mistaken for a staff token, and the tenant-versus-platform split is explicit in code rather than a flag on a shared claim set.

Supporting pieces:

  • Refresh tokens and one-time passwords live in Redis with expiry, so revocation and short-lived codes do not need Postgres writes.
  • Both the platform console and the Organization Workspace sit behind one shared login, with the token shape deciding which surface a person lands in.
  • Staff-only endpoints, such as the mobile configuration update, are restricted to platform tokens and audit logged.
  • Mobile sign-in with Google (both platforms) and Apple (iOS) is the remaining auth work before the mobile apps are complete.

Streaming pipeline

Anchor does not run its own media servers. Mux handles live ingest, transcoding and adaptive playback, and Anchor owns everything around it: who may watch, how streams relate to events and tickets, and what happens to recordings.

Live path

  1. An organizer creates an event in the Organization Workspace, and Anchor creates the matching live stream through Mux.
  2. The organizer streams to Mux, and viewers watch through the viewer web app and mobile apps.
  3. Mux sends webhooks for stream lifecycle changes. Anchor receives them, puts them on the message bus, and workers update event state.
  4. Access to paid streams is decided by Anchor from the viewer's ticket or pay-per-view purchase, not by the player.

Analytics

  • Mux custom dimensions carry tenant context (organization, event or session, plan tier, and whether the stream is simulcast), so analytics can be split per organization.
  • The ingestion path is the daily CSV Export API into an analytics schema in PostgreSQL. This integration is designed, and the schema additions are pending.

Archive and replay (designed, partly in progress)

  • Finished recordings move to Backblaze B2 as a cheaper archival tier, triggered by Mux webhooks. B2's S3-compatible API was chosen over its native API so existing S3 tooling can be reused.
  • A Cloudflare layer in front of B2 is planned for zero-egress delivery of archived assets.
  • B2 already serves as the export store.

Deployment

Anchor runs on Oracle Cloud Infrastructure. It started on AWS with Supabase and was migrated to OCI.

  • Network. A private network split into public, application and data tiers, with traffic between tiers restricted and no public administrative access.
  • Compute. The API and its workers run separately from the cache and message broker.
  • Database. Managed PostgreSQL in the data tier.
  • Edge. A load balancer terminates TLS, with certificates issued and renewed automatically.
  • Packaging. Everything ships as Docker containers, built and deployed to production.
  • Process. Work is tracked as backend, frontend and infrastructure tasks, with feature branches, team pull requests and code review.
  • Health. A health endpoint on the production API reports status and environment, and is used to confirm each rollout.

What I would change

Process supervision. Load testing with k6 showed a two-population failure pattern: most requests failing in milliseconds alongside a small set succeeding with very high latency. The leading hypothesis is that a crashed worker process is never restarted because nothing supervises it. The fix I would make is putting a process manager in front of the application workers. The diagnosis is still being confirmed, so this comes first, before any performance work.

Connection pooling. PgBouncer is not deployed yet. With schema-per-tenant, pooling must be configured so the per-request search_path cannot bleed between requests, for example by setting it per transaction.

A staging environment. There is none today, so changes go from dev to production with no realistic rehearsal. It would also unlock environment-specific settings, such as separate mobile configuration, which are deferred for now.

A WAF on the API layer. The choice between OCI WAF, Cloudflare WAF and self-hosted ModSecurity is still open. The WAF belongs on the backend API layer only.

Tenant migration tooling. A handful of schemas is trivial. Hundreds would not be. I would invest early in a migration runner that applies changes tenant by tenant, reports failures, and can resume.

Reducing key-person risk. One engineer built and maintains most of the stack. Runbooks, a documented deploy process, and moving frontend tasks to the dedicated frontend engineer are higher value than any new technology.

Product gaps. Cross-organization event discovery, unified search and a real notifications backend need scope decisions. Saving buyer card authorizations for repeat ticket purchases is a potential feature.

What I deliberately would not change. A proposed rewrite of the API layer in Go was considered and rejected. The root cause of the performance problem was unidentified, the API is I/O-bound so Go offers little advantage, and a second stack would duplicate auth, RBAC and the schema-per-tenant routing for a solo maintainer. The modular monolith stays until measurements say otherwise.