0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • Introduction
  • Pitch
  • Hogwarts
  • Live Demo
  • MVP
  • Roadmap
  • Launch Sprint
  • PRD
  • Get Started
  • Localhost
  • Architecture
  • Structure
  • Pattern
  • Page
  • Layout
  • Content
  • Types
  • Config
  • Actions
  • Queries
  • Authorization
  • Validation
  • Form
  • Table
  • Detail
  • Card
  • Util
  • Hooks
  • List Params
  • Views
  • README.md
  • ISSUE.md
  • Technology Stack
  • Database
  • File
  • CDN Assets
  • Entry Points
  • Dashboard
  • Authentication
  • Credentials
  • OAuth
  • Flow Diagrams
  • Multi-Tenancy
  • Offline
  • Onboarding
  • Onboarding Videos
  • Add Values
  • Admission
  • Application
  • Attendance
  • Compliance
  • Profile
  • Exams
  • Exam Wizard
  • Timetable
  • Classrooms
  • Notifications
  • Conference
  • LMS (Lumos)
Finance
  • Finance
  • Fee Management
  • Invoice
  • Wallet
  • Salary
  • Payroll
  • Timesheet
  • Expenses
  • Budget
  • Receipt
  • Accounts
  • Banking
  • Reports
  • Dashboard
  • Permissions
  • Messages
  • Integration Flow
  • Provision
  • AI Document Processing
  • Document Intelligence
  • Internationalization
  • Translation
  • Translation Guide
  • Icons
  • Docs Factory
  • Inspiration
  • Listings
  • Teachers
  • Students
  • Catalog
  • Library
  • Contributing
  • Code of conduct
  • GitHub Workflow
  • Database Seeds
  • Database Safety
  • Test Accounts
  • Playwright
  • Prettier
  • Block Rebound
Sales & GTM
  • Marketing Brief
  • Sales
  • Go-to-market
  • Marketing
  • Admission — Feature Spotlight
  • Pilot Program
  • Leads
  • Proposal
  • Outreach Templates
  • Case Study
  • Competitors
  • Landing-page teardown
  • Competitor FAQ
  • Business model
  • Shared economy
  • Traction
Fundraising & Ecosystem
  • Get Support
  • Investor Deck
  • Data Room
  • Investors
  • Accelerators
  • Incubators
  • Grants
  • Sponsors
  • Partners
  • Competitions & Hackathons
  • Universities & Training Centers

OAuth

PreviousNext

Google and Facebook OAuth flow — provider config, multi-tenant adapter, account linking, callback preservation, cross-subdomain cookies.

OAuth providers (Google OIDC, Facebook OAuth 2.0) feed through the multi-tenant Prisma adapter and reuse the JWT/session callbacks documented in authentication. This page covers the OAuth-specific pieces.

Flow

User clicks button (login or join)
  → Social component onClick(provider)
  → store callback URL (3 layers: server cookie, sessionStorage, client cookie)
  → signIn(provider, { callbackUrl, state })
  → Provider (Google / Facebook) → consent → redirect
  → /api/auth/callback/{provider}
  → MultiTenantPrismaAdapter creates / links user
  → JWT issued, session populated
  → redirect callback resolves destination

The <Social /> component (src/components/auth/social.tsx) is used by both /login and /join.

Social component

Two branches in onClick(provider):

Subdomain context

When on a subdomain (e.g. demo.localhost:3000) or ?tenant=:

  1. Extract subdomain from hostname or tenant search param.
  2. Store in sessionStorage (oauth_tenant, oauth_callback_url).
  3. signIn(provider, { callbackUrl: dashboardUrl?tenant=... }).
const isProdSubdomain =
  currentHost.endsWith(".databayt.org") && currentHost !== "ed.databayt.org"
const isDevSubdomain =
  currentHost.includes(".localhost") && currentHost !== "localhost"

Main domain

When on ed.databayt.org or localhost:3000:

  1. Resolve callbackUrl from search params or fall back to DEFAULT_LOGIN_REDIRECT.
  2. Triple-store callback URL.
  3. Add base64-encoded state { callbackUrl, timestamp }.
  4. signIn(provider, { callbackUrl, redirect: true, state }).

Facebook #_=_ cleanup

Facebook appends #_=_ to the redirect. cleanUrlHash() strips it on mount; the redirect callback strips it again server-side.

if (window.location.hash === "#_=_") {
  const cleanUrl = window.location.href.replace(/#.*$/, "")
  window.history.replaceState({}, document.title, cleanUrl)
}

Providers

src/auth.config.ts registers Google and Facebook. The canonical pattern (Google):

Google({
  clientId: env.GOOGLE_CLIENT_ID || "",
  clientSecret: env.GOOGLE_CLIENT_SECRET || "",
  authorization: {
    params: {
      prompt: "consent",
      access_type: "offline",
      response_type: "code",
    },
  },
  profile(profile) {
    return {
      id: profile.sub,
      username: profile.name,
      email: profile.email,
      image: profile.picture,
      emailVerified: new Date(), // Google emails pre-verified
    }
  },
})

Google forces prompt: "consent" + access_type: "offline" for refresh tokens. Facebook follows the same shape with clientId/clientSecret; it does not require a custom profile mapper.

Multi-tenant adapter

The User model uses @@unique([email, schoolId]), not @unique email. The standard adapter's findUnique({ where: { email } }) fails. MultiTenantPrismaAdapter overrides four methods:

MethodBehavior
getUserByEmail(email)findFirst({ where: { email, schoolId: null } }) — finds OAuth users not yet assigned to a school
createUser(data)Creates with schoolId: null, maps name → username
getUserByAccount({ provider, providerAccountId })Lookup via Account's @@unique([provider, providerAccountId])
linkAccount(account)Creates Account row linking provider to user

OAuth users start with schoolId: null; assigned during onboarding.

Account linking

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@unique([provider, providerAccountId])
}

First sign-in:

  1. getUserByAccount — not found.
  2. getUserByEmail — checks for existing user (schoolId: null).
  3. If exists → linkAccount. Otherwise → createUser then linkAccount.

Repeat sign-in: getUserByAccount finds the row, returns the user, no writes.

OAuthAccountNotLinked: returned when a credentials user tries OAuth with the same email — security feature; user must sign in with their original provider first.

Callback URL preservation

OAuth bounces the user to the provider, then back to /api/auth/callback/{provider} — the original destination is lost. social.tsx triple-stores it before redirect:

LayerStorageSurvives redirect?
1Server httpOnly cookie via POST /api/auth/store-callbackYes
2sessionStorage[oauth_callback_intended]No (different origin)
3Client cookie oauth_callback_intended (15 min, SameSite=Lax)Yes
// /api/auth/store-callback
cookieStore.set({
  name: "oauth_callback_intended",
  value: callbackUrl,
  httpOnly: true,
  sameSite: "lax",
  secure: process.env.NODE_ENV === "production",
  maxAge: 900,
  path: "/",
})

resolveCallbackUrl() (in auth.ts) reads back, first match wins:

  1. Server cookie oauth_callback_intended
  2. URL searchParams (callbackUrl, redirect)
  3. Regex match for callbackUrl=...
  4. baseUrl searchParams

JWT and session for OAuth

src/auth.ts callbacks:

TriggerBehavior
signIn (initial)Receives AdapterUser with id, email, name only — no role/schoolId yet. Sets token id, provider, sessionToken, hash.
No trigger (subsequent)Detects !token.schoolId && token.id → refreshes from DB. Picks up role/schoolId once user record updates (post-onboarding).
update (manual)Loads schoolId and role from DB — used after onboarding assigns the user.

signIn callback always returns true. Provider checks: Facebook logs error if no email (missing email scope); Google logs warning if email_verified is false.

Key implication: the first request after OAuth login may have an incomplete session (no role/schoolId); it auto-refreshes from the DB on the next request.

Redirect callback

Full priority chain in authentication. OAuth-relevant helpers:

FunctionPurpose
resolveCallbackUrl(url, baseUrl)Multi-source resolution
validateCallbackUrl(callbackUrl, baseUrl)Same-origin security check
detectSubdomainFromHost(host)Extract subdomain (null for main domain)
getSmartRedirectUrl(url, baseUrl)JWT → school lookup → subdomain URL
constructSchoolUrl(subdomain, path)Environment-aware URL builder
extractLocaleFromUrl(url)Extract ar or en (default ar)

Cookie configuration

All auth cookies share domain: ".databayt.org" in production for SSO across ed.databayt.org and every school subdomain. Development uses domain: undefined — scoped to single hostname.

CookieNamehttpOnlymaxAgePurpose
PKCE verifierauthjs.pkce.code_verifierYes15minOAuth PKCE flow
Session tokenauthjs.session-tokenYes24hJWT session
Callback URLauthjs.callback-urlNo—NextAuth callback
CSRF tokenauthjs.csrf-tokenYes—CSRF protection
Stateauthjs.stateYes—OAuth state
Nonceauthjs.nonceYes—OIDC nonce

sameSite: "lax" everywhere (required for OAuth redirects); secure: true in production. trustHost: true is required for Vercel proxy environments.

Environment

VariableRequiredValidationUsed by
GOOGLE_CLIENT_IDFor Googlet3-env optional, ends .apps.googleusercontent.comauth.config.ts via env
GOOGLE_CLIENT_SECRETFor Googlet3-env optionalauth.config.ts via env
FACEBOOK_CLIENT_IDFor Facebookt3-env optional, numericauth.config.ts via process.env
FACEBOOK_CLIENT_SECRETFor Facebookt3-env optionalauth.config.ts via process.env
AUTH_SECRETYesMin 32 charsNextAuth core
NEXTAUTH_URLRecommendedInferred in v5NextAuth
NEXT_PUBLIC_APP_URLYesRequiredEmail links, callbacks

auth-config-validator.ts validates at module load.

Errors

OAuth failures redirect to /error?error={code}. ErrorCard (src/components/auth/error-card.tsx) maps codes:

CodeTrigger
ConfigurationMissing/invalid provider config
AccessDeniedProvider denied access
OAuthSigninFailed to start OAuth flow
OAuthCallbackCallback processing failed
OAuthCreateAccountDB error during user creation
OAuthAccountNotLinkedCredentials user tried OAuth with same email
CallbackGeneric callback error
defaultAnything else

File map

UI
  src/components/auth/social.tsx        # Social login buttons
  src/components/auth/error-card.tsx    # Error display

Config
  src/auth.config.ts                    # Provider configs
  src/auth.ts                           # Callbacks, cookie config

Adapter
  src/lib/multi-tenant-prisma-adapter.ts

API
  src/app/api/auth/store-callback/route.ts
  src/app/api/auth/[...nextauth]/route.ts

Validation
  src/lib/auth-config-validator.ts
  src/env.mjs

Middleware
  src/proxy.ts                          # Skips /api/auth/*

Routes / models / pages
  src/routes.ts
  prisma/models/auth.prisma             # User, Account
  src/app/[lang]/(auth)/{error,login,join}/page.tsx

See also

  • Authentication
  • Credentials
  • Flow diagrams
CredentialsFlow Diagrams

On This Page

FlowSocial componentSubdomain contextMain domainFacebook #_=_ cleanupProvidersMulti-tenant adapterAccount linkingCallback URL preservationJWT and session for OAuthRedirect callbackCookie configurationEnvironmentErrorsFile mapSee also

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.