Univa Learning

Module 7 of 9 Full stack

The Freemium Indie Stack, Applied

Level Beginner to Intermediate Estimated study time 2-3 evenings Prerequisites none beyond general familiarity with building a website
Video lesson: coming soon

Why this matters for Univa

This module is not theory. It is Univa's own toolbox, named and mapped. univa.my deploys to Cloudflare Pages via GitHub Actions and wrangler. Client apps run on Next.js, Supabase, and Vercel per docs/app-building-philosophy.md. Some client work already uses ToyyibPay for Malaysian payments with DuitNow QR. The point of this module is to turn "the stack that already works" into "the stack Ahnaf can explain, defend, and extend on purpose," including the pieces not yet in daily use: Resend, CHIP, shadcn/ui as a deliberate choice rather than a default, and a clear answer for crash/error reporting on the web (Firebase Crashlytics is not it).

Knowing this stack cold also pays off commercially: SME clients ask "why this tool and not that one," and "it's free and it works" is a weaker answer than "here is what each layer does, here is what it costs at your scale, and here is when we would need to change it."

Core concepts

The stack as a set of layers, not a single tool

A freemium indie stack is not one platform doing everything. It is a set of specialized layers, each owned by a different vendor, wired together by the developer:

LayerToolWhat it owns
IDEVS CodeWhere the code gets written, locally
Version control + CIGitHub (+ Actions)Source of truth for code; runs tests/builds/deploys on every push
Static hosting, edge functions, storage, DNSCloudflareServes the site fast worldwide, runs light edge logic, owns the domain
DeploymentVercelBuilds and hosts Next.js apps, one git push away from live
Auth + databaseSupabaseManages who a user is and what data they can see
Transactional emailResendSends the emails an app triggers (welcome, receipt, reset password)
UI componentsshadcn/uiPre-built, ownable React components, not a hosted service
PaymentsStripe / CHIP / ToyyibPayTakes money from a customer and tells the app it happened
DesignFigmaWhere the interface gets designed before it gets built
Crash/error reportingFirebase Crashlytics (mobile) / Sentry (web)Tells the team when something broke, and for whom

Two deploy targets, one decision

Cloudflare and Vercel both host applications, and Univa genuinely uses both, for different reasons:

  • Cloudflare Pages hosts univa.my: a marketing/brochure site plus light dynamic logic in Workers. Cloudflare wins here on cost (generous free tier, zero R2 egress) and edge reach.
  • Vercel hosts client Next.js apps, because Vercel is built specifically around Next.js: zero-config builds, preview deployments per pull request, and first-class support for the framework's server components and API routes.

The decision rule in practice: Next.js app with real backend logic and a team that will grow it → Vercel. Static or near-static site with simple edge logic → Cloudflare Pages. Do not run the same app on both; pick one deploy target per project and let the other layers (Supabase, Resend, payments) plug into whichever one was chosen.

shadcn/ui is not a hosted service

Every other tool in this list is a company running infrastructure Univa depends on. shadcn/ui is different: it is a CLI that copies component source code (buttons, forms, dialogs, tables) directly into the project's own codebase, built on Tailwind and Radix primitives. There is no shadcn server to go down, no shadcn free tier to exceed, and no shadcn bill. The trade-off is that updates are manual (there is no package to npm update), and Univa owns and maintains every component it pulls in. This fits the freemium philosophy precisely: no recurring cost, but also no external dependency in production.

Auth, RLS, and Realtime in one box

Supabase bundles three things Univa would otherwise stitch together from separate vendors:

  • Auth: sign-up, login, magic links, OAuth providers, session/JWT management.
  • Postgres with Row Level Security (RLS): a real relational database where access control is enforced inside the database itself, as SQL policies, not just in application code. A user's JWT carries their identity into every query; a policy like "users can only see their own rows" runs on every request whether or not the application code remembers to check.
  • Realtime: subscribe to database changes over a websocket, so a UI updates live when data changes, without polling.

The RLS point matters enough to repeat: application-level access checks can be forgotten in one code path and enforced in another. RLS policies live with the data, so they apply everywhere, including a client hitting the database directly.

Payment gateways: three different shapes of the same job

All three gateways covered here do the same core job (take a customer's money, tell the app it happened), but they differ in market, fee shape, and integration effort:

  • Stripe is a global, developer-first gateway. Extremely polished SDKs and docs, but its Malaysian rates (3% + RM1 domestic cards/FPX, more for international cards and currency conversion) are the highest of the three for a purely local SME transaction.
  • CHIP is a Malaysia-based gateway with no setup or monthly fee, transparent per-method pricing, and native support for FPX, DuitNow QR, cards, and local e-wallets in one integration.
  • ToyyibPay is the gateway Univa already uses in client work. It charges a flat fee per transaction (not a percentage) for FPX, which makes it unusually cheap for small-ticket SME transactions, and supports DuitNow QR through specific API flags (enableDuitNowQR, chargeDuitNowQR) plus an activation step, per reference_toyyibpay_duitnow_qr in memory.

The right choice depends on transaction size and payment mix, covered in the comparison table below.

Crash reporting: Firebase Crashlytics is a mobile tool wearing a general name

Firebase Crashlytics is genuinely excellent, but it was built for native iOS and Android apps: it hooks into the mobile crash reporting APIs (symbolicated native stack traces, ANRs, non-fatal mobile exceptions). It is not the right tool for a Next.js web app. For web, Sentry is the equivalent: it captures unhandled JavaScript exceptions, API route errors, performance traces, and session replay, all in the browser and Node.js runtime context a web app actually runs in. Univa's rule going forward: Crashlytics only if shipping a native mobile app (React Native, Flutter); Sentry for every web app, including every client Next.js build.

Domains and DNS: one owner, many pointers

A domain has exactly one home for its DNS records, and Univa keeps that home on Cloudflare. univa.my's DNS lives in Cloudflare, which then points different subdomains and record types at whichever service actually serves that traffic:

  • The root domain and static site point at Cloudflare Pages itself.
  • Email for the domain routes through Cloudflare Email Routing, splitting mail to the right inboxes (per reference_univa_site_deploy_pipeline: ahnaf@, admin@, shafiq@, and a catch-all).
  • If a client project is deployed on Vercel instead, the domain's DNS still lives wherever it was registered (often Cloudflare), but a CNAME or A record is pointed at Vercel's servers for that specific subdomain or domain.

The concept to hold onto: registration and DNS management is one job, hosting is a different job, and a single domain can point different parts of itself (root site, a subdomain, email) at entirely different providers at the same time.

Secrets and environment variables across layers

Every layer in this stack needs its own credentials, and none of them should ever live in the codebase itself:

  • Supabase issues a public anon key (safe in client-side code, restricted by RLS) and a private service-role key (server-only, bypasses RLS, never shipped to the browser).
  • Resend, Stripe, CHIP, ToyyibPay each issue an API key or secret that authorizes the app to send email or move money on the account's behalf.
  • Sentry issues a DSN (a semi-public identifier for where to send error reports) plus an auth token for CI-time features like source map uploads.

The rule Univa already follows for its own tools applies here without exception: credentials live in .env (gitignored) locally, and in the platform's own environment variable dashboard (Vercel project settings, Cloudflare Pages environment variables, GitHub Actions repository secrets) in production. Nothing that can move money, send email, or write to a database as an admin belongs in a client-side bundle or a committed file.

A quick glossary

  • CDN (Content Delivery Network): a network of servers, like Cloudflare's, that caches and serves static content from the location closest to each visitor.
  • CI (Continuous Integration): automatically building, testing, and (optionally) deploying code every time it is pushed, so broken code is caught before it reaches production.
  • JWT (JSON Web Token): a signed token that proves who a user is, passed with each request so a backend or database can check identity without a separate login step every time.
  • Webhook: an HTTP callback a third-party service (a payment gateway, for example) makes to the app's own server when something happens on their end, like a completed payment.
  • DSN: the endpoint identifier an error-reporting SDK (Sentry) uses to know where to send captured errors.
  • CNAME record: a DNS record that points one domain or subdomain at another hostname, the mechanism used to hand a subdomain off to a hosting provider like Vercel.

The landscape (comparison tables)

Full stack, layer by layer

LayerToolFree tier headlineAlternative worth knowing
IDEVS CodeFree, foreverJetBrains, Cursor
Version control + CIGitHub + Actions2,000 CI minutes/month private repos, unlimited publicGitLab
Static hosting / edge / storage / DNSCloudflareGenerous free Pages + Workers + R2AWS (CloudFront + S3 + Route 53)
Deployment (Next.js)Vercel~100k invocations/month, non-commercialNetlify, Cloudflare Pages
Auth + DBSupabase500MB DB, 50k MAUFirebase (NoSQL, different model), PlanetScale
Transactional emailResend3,000 emails/month (100/day cap)Postmark, SendGrid
UI componentsshadcn/uiFree, source-ownedMaterial UI, Chakra
PaymentsStripe / CHIP / ToyyibPaySee payments table belowBillplz, Curlec
DesignFigma3 files (Starter)Penpot (open source)
Crash/error reportingSentry (web)5,000 errors/month, 1 userLogRocket, Bugsnag

Payment gateways compared (Malaysia)

GatewaySetup/monthly feeFPX feeDuitNow QRCardsNotes
StripeNone3% + RM1 (domestic)Not natively (needs a bridge)3% + RM1 domestic, +1% international, +2% FXBest global SDK/DX; most expensive for a pure local FPX/DuitNow flow
CHIPNoneRM1 (B2C) / RM2 (B2B) flat1% (min RM0.15)2% local credit / 1% local debit / 3% foreignAll local rails in one API; fees subject to 8% SST
ToyyibPayRM100/year from year 2, no setup feeRM1 (B2C) / RM2 (B2B) flatSupported via enableDuitNowQR + chargeDuitNowQR + activationLimited/varies by planCheapest for small-ticket local FPX; Univa's current default

Note: gateway fees change; verify current rates on the provider's pricing page before quoting a client.

Crash/error reporting: Crashlytics vs Sentry

Firebase CrashlyticsSentry
Primary targetNative mobile (iOS/Android)Web (JS/TS, Node.js), plus mobile SDKs
What it capturesNative crashes, ANRs, non-fatal exceptionsUnhandled exceptions, API errors, performance traces, session replay
Free tierFree forever (Spark plan)5,000 errors/month, 1 user
Univa's useOnly if shipping a native mobile appDefault for every web app, including client Next.js builds

A request's journey

Trace one real action end to end: a customer on a Malaysian SME's site signs up, then makes a purchase.

  1. DNS and first byte. The browser resolves the domain through Cloudflare's DNS, then Cloudflare's edge network (or Vercel's CDN, depending on which deploy target the app uses) serves the static shell and assets from a point of presence near the user.
  2. The UI renders. The page is built from shadcn/ui components, styled with Tailwind, code that lives in the app's own repository, not fetched from a third party at runtime.
  3. Sign-up. The user submits the sign-up form. The client calls Supabase Auth, which creates the user record and issues a JWT session token.
  4. Data access is enforced. Every subsequent database read or write carries that JWT. Row Level Security policies inside Postgres decide what this specific user is allowed to see or change, independent of whatever the frontend code does or forgets to check.
  5. Checkout. The user buys something. The app calls ToyyibPay (or CHIP, or Stripe) to create a payment bill/session and redirects the user to complete payment (FPX, DuitNow QR, or card).
  6. The webhook. Once payment completes, the gateway calls back a serverless function (a Vercel API route, or a Cloudflare Worker if the app is Cloudflare-hosted) with a webhook confirming the transaction.
  7. The database updates. That webhook handler writes the confirmed order into Supabase Postgres, typically using a service-role key that bypasses RLS for this one trusted, server-side write.
  8. The email fires. The same handler (or a follow-up function) calls Resend to send a receipt or confirmation email to the customer.
  9. If something breaks. A client-side JavaScript error, or a server-side exception in the webhook handler, gets captured by Sentry, which alerts the team with a stack trace and enough context to reproduce it.
  10. Behind all of it. Every line of code in this journey reached production because it was pushed to GitHub, built and tested by GitHub Actions, and deployed automatically to Vercel or Cloudflare Pages. And before any of it was built, the checkout flow and email templates existed first as a Figma file.

Free-tier limits and the first paid upgrade

ServiceFree tierFirst thing that forces an upgrade
Cloudflare Pages/WorkersUnlimited static requests; Workers: 100,000 requests/day, 10ms CPU/requestCPU-heavy Worker logic, or exceeding the daily request cap → Workers Paid, $5/month
Vercel (Hobby)~100k function invocations/month, 100GB bandwidth, 10s function timeout, non-commercial use onlyGoing commercial (Hobby's terms require it), needing longer function timeouts, or needing team seats → Pro, from $20/month/seat
Supabase500MB database, 50k monthly active users, 1GB file storage, 5GB egress, project pauses after 7 days idle, 2 active projectsDatabase growing past 500MB, needing the project to never pause, or needing more than 2 active projects → Pro, $25/month
Resend3,000 emails/month, capped at 100/day, 1 verified domainHitting the 100/day cap (common even at low volume) or needing a second domain → Pro, $20/month
GitHub2,000 Actions minutes/month on private repos (unlimited on public repos), 500MB artifact storageCI minutes running out on an active private repo → pay-per-minute or Team plan
StripeNo monthly fee; pay-per-transaction (3% + RM1 domestic)No free-tier ceiling as such; the "upgrade" is really a rate negotiation once volume gets large
CHIPNo setup/monthly fee; pay-per-transaction (RM1-2 FPX flat, % on cards/e-wallets/DuitNow)Same as Stripe: no ceiling, cost scales with transaction volume and mix
ToyyibPayNo setup fee; RM100/year from the second year; RM1-2 flat per FPX transactionThe yearly fee itself, and any need for features gated to a higher plan (e.g. B2B-specific tooling)
Figma3 design files, unlimited drafts, full editorNeeding a 4th real project file, or multiple paid editor seats → from ~$12-20/month/seat
Firebase CrashlyticsFree forever (Spark plan)Rarely Crashlytics itself; upgrading to Blaze (pay-as-you-go) usually happens because of other Firebase products used alongside it
Sentry5,000 errors/month, 1 user, 30-day retentionError volume climbing past 5k/month, or needing a second team member on the account → Team plan, from ~$26/month

The pattern worth noticing: infrastructure layers (Cloudflare, Vercel, Supabase, GitHub) have hard usage ceilings that force a plan upgrade. Payment gateways have no ceiling at all, they just take a cut of every transaction from day one, so "free tier" does not really apply to them the same way.

How to choose

  • If the project is a static or near-static brand/marketing site, then deploy to Cloudflare Pages; skip Vercel entirely.
  • If the project is a Next.js app with real backend logic (auth, database writes, API routes), then deploy to Vercel, and reserve Cloudflare for the domain, DNS, and any edge-specific logic.
  • If the client needs email login, and row-level data access control, then use Supabase Auth + RLS rather than hand-rolling access checks only in application code.
  • If the app sends transactional email (receipts, password resets, welcome emails), then use Resend from day one; do not wire email through a general SMTP provider that was not built for deliverability at this scale.
  • If a client's payment volume is small-ticket and mostly local (FPX, DuitNow), then default to ToyyibPay (flat, cheap per-transaction fee) or CHIP (also flat/low, plus broader method support and no yearly fee).
  • If a client needs international cards, subscriptions, or a globally polished checkout experience, then use Stripe, and accept its higher local rate as the cost of that polish and reach.
  • If the app needs pre-built, customizable UI fast, then pull components from shadcn/ui rather than installing a heavier component library; it stays in the codebase with no external dependency.
  • If the build is a web app of any kind, then wire up Sentry before launch, not after the first production incident; never reach for Firebase Crashlytics unless it is a native mobile app.
  • If the design has not been reviewed with the client yet, then build it in Figma first; do not skip straight to code for anything client-facing.
  • If a credential can send money, send email, or write to the database as an admin, then it goes in .env and the hosting platform's secrets dashboard, never in a client-side bundle or a committed file.
  • If a project's domain needs to serve email plus a website plus a client app on different subdomains, then keep DNS management centralized (Cloudflare), and point each subdomain's record at whichever service actually hosts that piece.

Univa playbook

Univa's default stack for a new SME client build, in order of how a request actually flows:

  1. Figma for the design, reviewed with the client before code starts.
  2. VS Code locally, GitHub for version control, GitHub Actions for CI (lint, type-check, build) on every push.
  3. Next.js + TypeScript strict + Tailwind + shadcn/ui for the app itself.
  4. Vercel for deployment (or Cloudflare Pages if the project is closer to a static brochure site, like univa.my itself).
  5. Supabase for auth, Postgres, RLS, and Realtime where the app needs live updates.
  6. Resend for every transactional email the app sends.
  7. ToyyibPay as the default Malaysian payment gateway for small-ticket local transactions, with CHIP as the fallback comparison when a client's transaction mix leans toward DuitNow QR or e-wallets, and Stripe only when the client explicitly needs international cards or a global-grade checkout.
  8. Sentry wired in before launch for every web build; Firebase Crashlytics reserved strictly for native mobile projects, which is not most of what Univa ships.

This is the same stack already running univa.my and client apps today; the change from this module is precision, not direction: knowing exactly which free-tier ceiling gets hit first on each client build, and being able to say so before it happens instead of after.

Common pitfalls to watch for on future builds:

  • Quoting a client a payment gateway fee from memory instead of checking the current pricing page; all three gateways change rates without much notice.
  • Leaving a Supabase service-role key inside a client-side file or a public repo instead of a server-only environment variable.
  • Letting a Resend-sending app hit the 100-emails/day cap unnoticed, because the monthly cap (3,000) looked like the only limit that mattered.
  • Reaching for Firebase Crashlytics on a web project out of familiarity, when Sentry is the tool actually built for that runtime.
  • Deploying the same app to both Cloudflare Pages and Vercel "just in case," instead of picking one target and keeping the setup simple.
  • Splitting DNS management across two providers for the same domain "temporarily," which creates confusion about where a record actually lives when something breaks.

Hands-on exercise

Pick one small idea (a waitlist page is enough) and build the full journey end to end in one evening:

  1. Design a one-page waitlist form in Figma (email field, submit button, confirmation state).
  2. Scaffold a Next.js app with shadcn/ui components matching the design, push it to a new GitHub repo, and confirm GitHub Actions runs a build on push.
  3. Deploy it to Vercel.
  4. Add Supabase: a waitlist table with an RLS policy that only allows inserts (no one can read anyone else's email back).
  5. On successful sign-up, call Resend to send a confirmation email.
  6. Add Sentry and deliberately trigger a client-side error (throw inside a button handler) to confirm it shows up in the Sentry dashboard.
  7. Store every API key (Supabase, Resend, Sentry DSN) in .env locally and in Vercel's environment variables in production; confirm .env is gitignored before the first push.
  8. Write down which free-tier limit, from the table above, this exact project would hit first if it suddenly went viral.

Self-check

Q1. Why does Univa use both Cloudflare Pages and Vercel instead of picking just one?

Q2. What does Row Level Security actually enforce, and why is it stronger than checking permissions only in application code?

Q3. Between Stripe, CHIP, and ToyyibPay, which is cheapest for a small Malaysian SME taking mostly FPX payments, and why?

Q4. Why is Firebase Crashlytics the wrong default choice for a Next.js web app, and what should be used instead?

Q5. In the request's journey, at what point does a service-role key (bypassing RLS) get used, and why is that acceptable there specifically?

Q6. A client's domain is registered and DNS-managed on Cloudflare, but the actual app is deployed on Vercel. What has to happen for the domain to point at the right place, and does the DNS management need to move to Vercel?

Answers:

  • A1. Cloudflare Pages fits static/near-static sites (like univa.my) with a strong free tier and edge reach; Vercel is purpose-built for Next.js apps with real backend logic, offering zero-config builds and preview deployments. Each project picks one target based on what it actually needs, rather than forcing every project onto the same platform.
  • A2. RLS enforces access control inside the database itself, as SQL policies tied to the authenticated user's JWT, so it applies to every query against that table regardless of which code path issued it. Application-level checks can be missed in one route and enforced in another; RLS cannot be bypassed by a forgotten if statement in the frontend.
  • A3. ToyyibPay or CHIP, both of which charge a flat RM1 (B2C) per FPX transaction rather than a percentage; Stripe charges 3% + RM1 domestically, which costs more per transaction and scales worse with volume for small-ticket local payments.
  • A4. Crashlytics is built for native mobile crash reporting (iOS/Android stack traces, ANRs) and does not naturally capture web JavaScript exceptions or Next.js server errors. Sentry is built for exactly that environment and should be the default for any web app.
  • A5. The service-role key is used in the server-side webhook handler that confirms a completed payment and writes the order to the database. It is acceptable there because that code runs entirely on the server, is never exposed to the browser, and needs to write data on behalf of the system rather than on behalf of one logged-in user's own permissions.
  • A6. A CNAME (or A record) for that domain or subdomain gets added in Cloudflare's DNS, pointing at Vercel's hosting servers. DNS management does not need to move; registration and DNS stay on Cloudflare, only the specific record for that hostname changes to route traffic to Vercel.

Further reading