Financial Education
LMS Platform for Brazil
Technical analysis of architecture options for ATFX's Learning Management System. We evaluated two paths — a Modern Stack and WordPress — and present our findings below, including trade-offs, compliance considerations, and cost projections.
The Challenge
ATFX Educacao needs a proprietary LMS platform to deliver financial education courses in Brazil. The platform must handle user authentication, paid course access, video delivery, and payment processing — all while complying with Brazil's LGPD data protection law (Lei 13.709/2018). The wrong architecture decision introduces legal liability, data portability risk, and potential rebuild costs.
- 01User registration and authentication for Brazilian students
- 02Paid course access with PIX, Boleto, and credit card payments
- 03Private video delivery with access control
- 04LGPD compliance: data residency in Brazil, consent management, data portability (Art. 18)
- 05Scalable infrastructure for LATAM expansion
Architecture Decision Record
We evaluated WordPress as a potential platform for the LMS. WordPress is a proven CMS with a large ecosystem and faster initial setup. However, our technical assessment identified specific limitations that are worth considering before making an architecture decision.
Access Control Incompatibility
MemberPress — the access control plugin required for private courses — does not officially support WordPress Multisite. The core payment + access architecture cannot be built as intended. Alternative plugins (Paid Memberships Pro) exist but add complexity and reduce reliability.
Data Fragmentation Risk
Course, user, and payment data are stored across separate third-party plugin schemas (LearnDash, WooCommerce, Paid Memberships Pro). ATFX would not own or control the database structure, making LGPD data portability (Art. 18) and future platform migrations significantly more difficult.
LGPD Granular Compliance
Granular LGPD compliance cannot be fully guaranteed through third-party plugins whose code, storage behavior, and data handling ATFX does not control. In a regulated financial market, this introduces legal uncertainty that can be avoided with a purpose-built architecture.
Note: WordPress remains the recommended tool for the public marketing landing page (Module 6). The Elementor-based landing page fetches public course data from Supabase via anon key — no authentication, no payments, no user data is stored in WordPress.
Modern Stack: Next.js + Supabase
An architecture where each component is selected for LGPD compliance, data ownership, and long-term scalability. ATFX would control the full stack — from database schema to payment flow to data export APIs.
System Architecture
Payment Flow (Stripe Elements — inline)
LGPD Compliance
Brazil's LGPD (Lei 13.709/2018) requires any platform processing personal data of Brazilian citizens to guarantee data residency, explicit consent, and portability rights. Both architecture options can achieve compliance, but the implementation path and long-term maintenance differ. Below is how the Modern Stack addresses each requirement.
- +Legal basis for data collection (user consent on signup)
- +Data stays in Brazil (Sao Paulo region hosting)
- +Collect only necessary data (no excessive tracking)
- +User rights honored (export, correct, delete data)
- +Encrypted access, automatic logging, data isolation
- +Privacy policy page in PT-BR
- +Payment via Stripe (PCI DSS Level 1 compliant)
Monthly Cost Projection
Costs are approximate and usage-based. Stripe charges 2.9% + $0.30 per transaction (not included in monthly totals). Stripe Brazil supports PIX, Boleto, and credit cards.
Modern Stack
If the initial architecture requires a full rebuild due to access control or compliance limitations:
Implementation Roadmap
6 Functional Modules
User signs up via Clerk -> Clerk webhook -> Supabase Edge Function creates user row -> Resend sends welcome email
Clerk + Webhook + Supabase Edge FunctionBrowser ↓ User submits signup form (email, password) ↓ Clerk.users.create() ↓ Clerk webhook triggered ↓ Supabase Edge Function receives webhook ↓ INSERT INTO users table ↓ Resend API: send welcome email ↓ ✅ User authenticated (JWT issued)
Admin logs into Payload CMS -> Creates course -> Uploads media directly to GCS via Signed URL -> Payload saves metadata + calls revalidateTag on Next.js
Payload CMS + GCS + Signed Upload URLsAdmin Portal ↓ Payload CMS (headless) ↓ Admin creates course → uploads video ↓ Signed URL generator (GCS) ↓ Upload directly to GCS bucket ↓ Payload saves metadata ↓ revalidateTag() on Next.js ↓ ✅ Course published + cached
User clicks Buy -> API route reads price from Supabase -> Creates Stripe Payment Intent -> Stripe processes payment -> Webhook confirms -> Purchase row created -> Realtime notifies frontend
Stripe Payment Intent + Webhook + Supabase RealtimeFrontend ↓ User clicks "Comprar Curso" ↓ POST /api/payments/create-intent ↓ Verify JWT + Query Supabase price ↓ stripe.paymentIntents.create() (amount, currency: BRL, PIX/Boleto) ↓ Return client_secret to frontend ↓ Render Stripe Elements inline ↓ User pays via PIX QR or Boleto (stays on platform) ↓ Stripe webhook confirmation ↓ ✅ Order recorded in Supabase
Layer 1: Clerk JWT verification -> Layer 2: Next.js middleware checks purchase in Supabase -> Layer 3: API route re-verifies -> Generates 15-min Signed URL
Clerk + Next.js Middleware + GCS Signed URLsRequest → middleware.ts ↓ Layer 1: Verify Clerk JWT token ↓ Layer 2: Check course enrollment in Supabase RLS ↓ Layer 3: Re-verify in API route ↓ Generate 15-min signed URL (GCS) ↓ Return video stream URL ↓ Browser plays video ↓ ✅ Access granted for enrolled users only
User updates name/bio (Supabase UPDATE). Avatar: API route deletes old file -> uploads new to GCS -> updates avatar_url. If upload fails, previous avatar preserved.
Supabase UPDATE + GCSUser dashboard ↓ Edit profile (name, bio) ↓ POST /api/profile/update ↓ UPDATE users table (Supabase) ↓ Avatar upload? → Delete old GCS file ↓ POST to GCS (signed URL) ↓ UPDATE avatar_url in Supabase ↓ If upload fails → Rollback to previous ↓ ✅ Profile synced
Separate project (Elementor/Next.js static). Uses Supabase anon key to read published courses only. RLS ensures anon key accesses only public fields.
Elementor (WordPress) + Supabase RLSPublic landing page ↓ Load separate Next.js project ↓ Use Supabase anon key ↓ Query published courses ↓ RLS policy: anon key → public fields only ↓ Display course catalog ↓ SEO optimized (Open Graph, Schema.org) ↓ ✅ Public-facing course list
System Architecture
3 independent projects + cloud services
Critical Flows
Security Best Practices
Testing Strategy
Deployment Checklist
- +All env vars configured in Vercel
- +Supabase RLS policies tested and verified
- +Stripe webhook endpoint configured for production
- +Clerk webhook endpoint configured for production
- +GCS private bucket + CORS configured
- +Rate limiting verified (Upstash Redis)
- +Sentry DSN configured for error monitoring
- +Custom domain configured in Vercel + DNS verified
- +Full E2E test suite passing before deploy
Acceptance Criteria (MVP Ready)
- 01User can register via Clerk -> Supabase row created
- 02Admin can create course in Payload -> uploads to GCS -> appears in catalog
- 03User can purchase course via Stripe -> webhook confirms -> access unlocked
- 04Only users with completed purchase can access content (3-layer verification)
- 05Signed URLs for media expire after 15 minutes
- 06All Brazilian user data resides in Sao Paulo region
- 07RLS policies enforced on all tables
- 08Webhook signature verification for Stripe + Clerk
- 09Rate limiting: 5 req/min Payment Intent, 20 req/min Signed URLs
- 10E2E test passes: full user journey from signup to content access