← All Posts
Technical·November 6, 2025·8 min

Authentication Done Right: Build It First, Build It Once

Auth is the foundation every feature sits on. Here's how we approach it — including a real Cognito implementation from Odunde.

Every project we take on at Aetheris starts with the same first task: authentication. Not the UI. Not the data model. Not the feature the client is most excited about. Auth.

This isn't arbitrary. Authentication is the foundation that every other feature depends on. User profiles, role-based access, data ownership, API authorization, session management — all of it sits on top of auth. Get it wrong early and you'll rebuild it later, painfully, with users in the system and data to migrate.

We've implemented auth systems across BookIt Sports, Footy Access, Edge Athlete OS, and Odunde. Here's everything we've learned — including a real implementation walkthrough from Odunde's Cognito setup — so you can get it right the first time.

Why Auth Comes First

The temptation is to stub out auth with a fake login and build the "interesting" features first. We've inherited codebases where teams did exactly this. The result is always the same:

  1. Features get built without proper authorization. API endpoints that should check permissions don't, because the auth system isn't real yet.
  2. Data models lack ownership. Records don't have proper user associations because there were no real users during development.
  3. The retrofit is brutal. Threading auth through an existing codebase means touching every route, every API call, every component that renders user-specific content.

Building auth first means every feature you build afterward automatically has proper user context, permission checks, and session handling. It's faster in total, even though it feels slower at the start.

The Provider Comparison: Cognito vs Auth0 vs Supabase Auth

We've evaluated (and used) all three. Here's an honest comparison for the types of applications we build.

AWS Cognito

What it is: AWS's managed authentication service. User pools handle sign-up, sign-in, and token management. Identity pools handle AWS resource access.

Strengths:

  1. Deep AWS integration. If your backend runs on AWS (Lambda, API Gateway, DynamoDB), Cognito slots in natively. IAM policies can reference Cognito groups directly.
  2. Cost at scale. The free tier covers 50,000 monthly active users. Beyond that, pricing is aggressive — fractions of a cent per MAU.
  3. Custom user attributes. You can define custom attributes on the user pool — roles, organization IDs, subscription tiers — without a separate database.
  4. Advanced security features. Adaptive authentication, compromised credential detection, and device tracking are built in.

Weaknesses:

  1. Documentation is AWS-grade. Technically complete, practically confusing. Expect to spend time deciphering it.
  2. Hosted UI is limited. The pre-built login pages are functional but ugly. You'll almost certainly build custom UI.
  3. Migration complexity. Moving users out of Cognito is painful. Password hashes aren't exportable, so users have to reset passwords during migration.

Our verdict: Best choice when you're already in the AWS ecosystem and need enterprise-grade features at scale pricing. This is what we chose for Odunde.

Auth0

What it is: A dedicated identity platform. The most feature-complete managed auth solution on the market.

Strengths:

  1. Developer experience. The SDKs are excellent. The documentation is the best in the auth space.
  2. Social login. Pre-built connections for every major provider. Google, Apple, Facebook, Twitter — they all work out of the box.
  3. Actions and Hooks. Custom logic during the auth flow (post-login, pre-registration) without managing infrastructure.
  4. Universal Login. A customizable hosted login page that handles the security-critical parts for you.

Weaknesses:

  1. Pricing scales steeply. The free tier is 7,500 MAU. After that, costs climb fast — especially if you need features like Organizations or Machine-to-Machine tokens.
  2. Vendor lock-in. Auth0's APIs are proprietary. Switching away means rebuilding your auth layer.
  3. Overkill for simple apps. If you just need email/password and maybe Google login, Auth0's complexity isn't justified.

Our verdict: Best choice for apps that need complex identity flows — multi-tenant B2B, enterprise SSO, or heavy social login requirements. The DX is unmatched, but you pay for it.

Supabase Auth

What it is: Auth built into Supabase's open-source Firebase alternative. Sits on top of PostgreSQL with Row Level Security.

Strengths:

  1. Tight database integration. Auth users are PostgreSQL rows. Row Level Security policies reference the authenticated user directly. This is elegant.
  2. Simple and fast. For straightforward auth needs, Supabase Auth is the fastest to implement.
  3. Open source. You can self-host if you need to. No vendor lock-in.
  4. Generous free tier. 50,000 MAU on the free plan.

Weaknesses:

  1. Less mature. Missing some enterprise features that Cognito and Auth0 have — advanced threat detection, breached password checking.
  2. Tied to Supabase. Using Supabase Auth without the rest of Supabase is possible but awkward.
  3. Custom flows are limited. Complex auth flows (multi-step verification, custom MFA providers) require more manual work.

Our verdict: Best choice if you're already using Supabase as your backend. The database integration is genuinely excellent. Less ideal as a standalone auth solution.

Session Management: JWT vs Session Cookies

This is a decision every auth implementation faces, and the industry has strong opinions in both directions. Here's ours.

JWTs (JSON Web Tokens)

JWTs are self-contained tokens that carry user identity and claims. The server doesn't need to look anything up — the token itself contains the information.

When we use them:

  1. API-to-API communication. Microservices validating requests from other services.
  2. Mobile apps. Where cookie-based sessions don't apply natively.
  3. Stateless API backends. When you genuinely need horizontal scaling without shared session state.

The risks:

  1. Revocation is hard. Once issued, a JWT is valid until it expires. You can't "log out" a JWT without a blocklist, which reintroduces server-side state.
  2. Token size. JWTs are larger than session IDs. With custom claims, they can get meaningfully large.
  3. XSS vulnerability in browsers. If you store JWTs in localStorage (don't), a cross-site scripting attack gives full access to the token.

Session Cookies

Server-side sessions with a cookie identifier. The session data lives on the server; the browser only holds a reference.

When we use them:

  1. Web applications with server-side rendering. Next.js apps with API routes — our bread and butter.
  2. When revocation matters. Deleting the session on the server immediately invalidates access.
  3. When simplicity wins. For straightforward web apps, session cookies are simpler to reason about and harder to misuse.

Our general rule: Session cookies for web applications, JWTs for mobile and API-to-API. We use short-lived access tokens (15 minutes) with longer-lived refresh tokens, regardless of which approach we choose.

Social Login: Easier Than You Think, Harder Than It Should Be

Every client wants Google login. Most want Apple login (Apple requires it if you offer any social login on iOS). Here's what the implementation actually involves:

  1. OAuth application setup. Register your app with each provider. Google is straightforward. Apple requires an Apple Developer account and a somewhat arcane configuration process involving Service IDs and private keys.
  2. Callback handling. Each provider redirects back to your app with an authorization code. Your backend exchanges this for tokens and user information.
  3. Account linking. What happens when a user signs up with email, then later tries to log in with Google using the same email? You need a strategy. We link accounts by verified email address — if the email matches and is verified by the provider, we merge.
  4. Profile data mapping. Each provider returns different data shapes. Google gives you a name and profile photo. Apple gives you a name only on the first login (seriously) and no photo. Normalize this into your user model.

For Edge Athlete OS, social login with Google and Apple reduced sign-up friction significantly. Athletes — especially younger ones — don't want to create another email/password combination. One tap and they're in.

Role-Based Access Control

Most applications need at least two roles: regular user and admin. Many need more. Here's how we implement RBAC cleanly:

  1. Roles live in the auth provider. In Cognito, we use groups. In Auth0, we use roles. The user's role is included in their token claims.
  2. Middleware enforces access. A single middleware function checks the user's role against the required role for each route or API endpoint.
  3. UI reflects permissions. Components conditionally render based on the user's role. Admin features are invisible to regular users — not just disabled, invisible.
  4. API independently validates. Never trust the client. Even if the UI hides an admin button, the API endpoint must independently verify the user's role before executing.

For Odunde, we needed multiple role tiers: attendees, vendors, organizers, and platform admins. Each tier has different access to different platform features. Cognito groups mapped cleanly to these roles, and our middleware layer made adding new permission checks trivial.

The Odunde Implementation: Cognito in Practice

Odunde is a festival and sports platform with multiple user types, event management, and vendor coordination. Here's how we implemented auth:

  1. Cognito User Pool with custom attributes. We defined custom attributes for user type (attendee, vendor, organizer, admin), organization affiliation, and onboarding status directly on the user pool.
  2. Cognito Groups for RBAC. Each role maps to a Cognito group. Group membership is included in the ID token, which our Next.js middleware reads on every request.
  3. Custom sign-up flow. Different user types have different sign-up requirements. Vendors need business information. Organizers need approval. We used Cognito's pre-sign-up Lambda trigger to validate and route registrations.
  4. Social login with account linking. Google and email/password auth with automatic account linking by verified email. A user who signs up with email can later sign in with Google seamlessly.
  5. Token management. Short-lived access tokens (1 hour), longer-lived refresh tokens (30 days), stored in HTTP-only secure cookies for the web app. The React Native mobile client stores tokens in secure storage.

The entire auth system — sign-up, sign-in, social login, role management, session handling, and password reset — was built and tested in five days. It's been stable in production with zero auth-related incidents since launch.

The Security Checklist

Before any Aetheris project goes to production, we verify every item on this list:

  1. HTTPS everywhere. No exceptions. All cookies have the Secure flag.
  2. HTTP-only cookies. Session tokens are never accessible to JavaScript.
  3. CSRF protection. SameSite cookie attribute plus CSRF tokens on state-changing requests.
  4. Rate limiting on auth endpoints. Login, registration, and password reset are rate-limited to prevent brute force and credential stuffing.
  5. Password requirements. Minimum 8 characters, checked against known breached passwords where the provider supports it.
  6. Email verification. Accounts aren't fully active until the email is verified.
  7. Secure password reset. Time-limited, single-use tokens. Reset links expire after 1 hour.
  8. Token rotation. Refresh tokens rotate on use. A stolen refresh token can only be used once.
  9. Audit logging. Authentication events (login, logout, password change, failed attempts) are logged with timestamps and IP addresses.
  10. Dependency updates. Auth-related dependencies are updated within 48 hours of security patches.

This isn't overkill. It's the minimum. Auth is the one part of your application where "good enough" isn't good enough.

The Bottom Line

Authentication isn't a feature — it's infrastructure. Build it first, build it right, and every feature you add afterward inherits proper user context, authorization, and session management for free.

Choose your provider based on your actual needs: Cognito for AWS-native projects at scale, Auth0 for complex identity requirements with excellent DX, Supabase Auth for tight database integration. Don't choose based on which tutorial you saw last.

And if you're tempted to "just use a simple JWT approach" and build auth yourself — don't. The surface area for security mistakes is enormous, and the managed providers have teams of security engineers whose entire job is to handle the edge cases you haven't thought of.

Build it first. Build it once. Move on to the features your users actually care about.

Building something? Let's talk.

We're always happy to talk through your situation — no commitment required.

Get in touch