6. Authentication Setup

Alex needs to let law firm employees sign into LexVault securely.

The Auth settings page lets you configure how end-users authenticate in your application. Eurobase provides a built-in email/password auth system — no external providers required.

Auth methods

  • Email + Password — traditional sign-up and sign-in with email and password
  • Magic Links — passwordless sign-in via a one-time email link (no password needed)
  • Phone (SMS OTP) — sign in with phone number via a 6-digit SMS code (EU-sovereign SMS via GatewayAPI). Paid plans only — every send has a per-message cost.
  • Passkeys — passwordless sign-in with Face ID, Touch ID or a security key (WebAuthn). Turn on in Auth → Settings and enter your app's domain and origins; in the SDK, registerPasskey() for a signed-in user and signInWithPasskey() (SDK 0.9.0 or later)
  • Social Login — Google, GitHub, LinkedIn, Apple (configure in Auth settings)

Configuration options

  • Password rules — set minimum length (8–128 characters)
  • Allow new signups — turn it off for an invitation-only app. Sign-up, OAuth and phone sign-in then create no new users: signUp() returns signups_disabled (403), OAuth redirects back with error=signups_disabled, and a code request for an unknown phone number answers as usual but sends nothing. Existing users sign in as before. Create users from the Users tab or from your server with the Admin users API (one at a time or as an import).
  • Email confirmation — require users to verify their email before signing in
  • Session duration — how long access tokens remain valid (1h to 30 days)
  • Redirect URLs — whitelist URLs your app can redirect to after auth callbacks
  • CORS origins — browser origins (scheme://host[:port]) allowed to call this project's API. Add your dev and production app origins. Eurobase platform origins (*.eurobase.app) are always allowed. Loopback port wildcard supported: add http://localhost:* (or the 127.0.0.1 / [::1] spellings) to match any port — the standard carve-out for local dev tools that bind a fresh random port each run. Wildcards on non-loopback hosts are rejected at save time.

Email confirmation — end-to-end

When you enable Email confirmation, signups aren't complete until the user clicks a link in the verification email. Same shape for password reset and magic-link sign-in. All three flows follow the same three-step pattern:

  1. User signs up / requests a reset / requests a magic link via the SDK.
  2. Eurobase sends an email with a link to a page on your app (not Eurobase's console) with a ?token=... query parameter.
  3. Your page reads the token from the query string and calls the corresponding SDK method to complete the flow.

Configure the URLs

In Auth → Settings → Email-flow redirect URLs, set the three URLs your app will host:

  • Email verification URL — e.g. https://yourapp.example/verify. Required when "Require email confirmation" is on.
  • Password reset URL — e.g. https://yourapp.example/reset-password.
  • Magic link URL — e.g. https://yourapp.example/magic-link.

Each URL must appear in the Allowed redirect URLs list above (same allowlist). The console blocks Save inline if a URL isn't allowed; the backend rejects the update for the same reason. This is the standard open-redirect defence — without it, an attacker could put your project's URL in a phishing email and land users on their own page. Add both your dev and prod URLs to the redirect list before configuring the three fields.

Your app's name in emails

Sign-in, confirmation and password emails name your app in the subject and the text (“Sign in to …”). By default that's the project name. To show a nicer name, set Auth → Settings → App name in emails (up to 60 characters). Custom email templates get the same value as {{.ProjectName}}.

The verify-email page (copy-paste starter)

This is the minimum — a static HTML file works. Adapt to your framework (Next.js page, SvelteKit route, Vue component — same three lines of logic).

<!-- verify.html -->
<!DOCTYPE html>
<html lang="en">
<head><title>Verifying your email…</title></head>
<body>
  <h1 id="status">Verifying…</h1>
  <script type="module">
    import { createClient } from 'https://cdn.jsdelivr.net/npm/@eurobase/sdk/+esm'
    const eb = createClient({
      url: 'https://your-project.eurobase.app',
      apiKey: 'eb_pk_YOUR_PUBLIC_KEY',
    })
    const token = new URL(location.href).searchParams.get('token')
    const status = document.getElementById('status')
    if (!token) {
      status.textContent = 'Missing verification token in the URL.'
    } else {
      const { error } = await eb.auth.verifyEmail(token)
      status.textContent = error ? 'Verification failed: ' + error : 'Verified!'
    }
  </script>
</body>
</html>

Before the user confirms

  • Sign-up returns no session. The response has the new user and confirmation_required: true, without tokens. In the SDK, signUp() returns data: null, confirmationRequired: true and user: show a "check your inbox" screen.
  • Sign-in with the correct password answers 403 {"error": "email_not_confirmed"} (SDK: error === EMAIL_NOT_CONFIRMED). Offer eb.auth.resendVerification(email). A wrong password still answers 401 invalid email or password, whether or not the account is confirmed.
  • Refresh answers the same 403 for a user who isn't confirmed (for example a session issued before this rule).
  • Reading the status: user objects carry email_confirmed_at (null until confirmed) — in getUser(), the sign-in response and the console's Users page.
  • Users who sign in with OAuth or a magic link count as confirmed (the link or the provider proves the address), and so does a completed password reset. Users you create in the console are confirmed. Phone sign-in and passkeys follow the same rule as a password sign-in for users who have a password.
  • Turning the setting on later: existing password users who never confirmed are refused at their next sign-in or token refresh, which signs them out. Confirm them first: Users → (user) → Mark confirmed, or the users API with {"email_confirmed": true}.
  • Apps must use @eurobase/sdk 0.10.0 or later with this setting: older versions treat the sign-up response as a session.

Password reset page

Same shape, but you collect a new password from the user first, then call eb.auth.resetPassword(token, newPassword). Trigger the email with eb.auth.forgotPassword(email).

Changing the password while signed in

A signed-in user changes their password with POST /v1/auth/user/password and their access token, sending current_password and new_password; the response is a fresh session. The SDK helper eb.auth.updatePassword({ currentPassword, newPassword }) needs @eurobase/sdk 0.11.0 or later. The current password is required, and wrong guesses count towards the account's sign-in attempt limit. Their refresh tokens are revoked, so other sessions end once their current access token expires; the current one gets a fresh session. Users who sign in without a password (magic link, OAuth) set one through a password reset. After every change — and after a completed password reset — the user gets a short "your password was changed" email, so they notice a change they didn't make. It uses the Password Changed template (customisable on Pro) and your project's sender, and shows up in the email log.

Magic link page

Same shape. Trigger with eb.auth.requestMagicLink(email); complete with eb.auth.signInWithMagicLink(token). On success the user is signed in and you can redirect to your app's home page. The email only goes to an address that already has an account (see Magic Links below).

Email log: did the email go out?

These requests answer “OK” whether or not an email was sent, so they never reveal which addresses have accounts. Auth → Email log shows what really happened to each one: sent (with the email service's id), not sent (no user with that address, redirect URL not allowed, already confirmed) or failed (with the reason, e.g. your SMTP server rejected the login). Addresses are shortened (p****@example.com) and entries are kept 14 days. Needs the Developer role.

Test with real mailboxes. Emails sent through Eurobase's email service share one sending reputation, so it doesn't send to addresses that can't receive mail: reserved test domains (example.com, .test, …) are skipped, and so is an address whose mail failed permanently (no such mailbox, unknown domain) for 30 days, or 7 days for a full mailbox. The email log shows these as not sent, with the reason. Your own SMTP sender (Pro) isn't affected.

Per-request override (multi-tenant apps)

If you serve multiple environments or subdomains from one project, pass emailRedirectTo on the SDK call to override the default for that user:

await eb.auth.signUp({
  email: 'user@example.com',
  password: 'SecurePass123!',
  emailRedirectTo: 'https://staging.yourapp.example/verify',
})

The per-request URL must still be in your Allowed redirect URLs list, or the signup returns 400 (same open-redirect defence).

Migrating from the broken default

If your project had Require email confirmation enabled before July 2026, signups were silently broken — the email linked to a Eurobase-hosted URL that never existed. If any users were created with an unconfirmed email in that window, run this SQL from the SQL Runner to check who's affected:

SELECT id, email, created_at FROM users
WHERE email_confirmed_at IS NULL
ORDER BY created_at ASC;

After you configure the three redirect URLs, those users can request a fresh verification email with eb.auth.resendVerification(email) from a "verify your email" prompt in your app.

SDK auth flow

// Sign up a new user
const { user, error } = await eb.auth.signUp({
  email: 'lawyer@acmelegal.eu',
  password: 'SecurePass123!'
})

// Sign in
const { session, error: signInError } = await eb.auth.signIn({
  email: 'lawyer@acmelegal.eu',
  password: 'SecurePass123!'
})

// Listen for auth state changes
eb.auth.onAuthStateChange((event, session) => {\
  console.log('Auth event:', event)  // 'SIGNED_IN', 'SIGNED_OUT', 'TOKEN_REFRESHED'
  console.log('Session:', session)
})

// Sign out
await eb.auth.signOut()

The SDK refreshes the session before the access token expires. The session ends (SIGNED_OUT) only when the server rejects the refresh token: it was revoked or expired, or the user must confirm their email first. If the network is down or the server has a temporary problem, the session is kept and the SDK retries with backoff. Browser tabs of the same app share one session: one tab refreshes, the others use the session it stores, and signing out in one tab signs out all of them. Requires @eurobase/sdk 0.11.0 or later.

Verifying tokens on your server

Access tokens are JWTs signed with ES256 using a key pair that belongs to your project. Your server can check them itself, with no call to Eurobase: fetch the public key once from https://your-project.eurobase.app/v1/auth/.well-known/jwks.json (public, no API key, safe to cache) and verify each token's signature, issuer (eurobase), audience (your project ID) and expiry. Any JWT library with JWKS support works; with jose in Node:

import { createRemoteJWKSet, jwtVerify } from 'jose'

const JWKS = createRemoteJWKSet(
  new URL('https://your-project.eurobase.app/v1/auth/.well-known/jwks.json')
)

// token = the user's access token (Authorization: Bearer …)
const { payload } = await jwtVerify(token, JWKS, {
  issuer: 'eurobase',
  audience: 'YOUR_PROJECT_ID',
  algorithms: ['ES256'],
})
// payload.sub = user id, payload.email (the phone number for phone sign-in), payload.exp

In Go, with keyfunc and golang-jwt:

// go get github.com/MicahParks/keyfunc/v3 github.com/golang-jwt/jwt/v5
// Create once at startup with a long-lived ctx (it runs the key refresh),
// not per request.
jwks, err := keyfunc.NewDefaultCtx(ctx, []string{
	"https://your-project.eurobase.app/v1/auth/.well-known/jwks.json",
})
if err != nil {
	return err
}

// tokenString = the user's access token (Authorization: Bearer …)
token, err := jwt.Parse(tokenString, jwks.Keyfunc,
	jwt.WithValidMethods([]string{"ES256"}),
	jwt.WithIssuer("eurobase"),
	jwt.WithAudience("YOUR_PROJECT_ID"),
	jwt.WithExpirationRequired(),
)
if err != nil {
	return err // invalid or expired: answer 401
}
claims := token.Claims.(jwt.MapClaims) // claims["sub"] = user id

Both libraries cache the key, refresh it periodically, and fetch it again when a token names a key they don't have yet. Verifying locally is optional: GET /v1/auth/user keeps working for every token.

A token stays valid until it expires, even after the user signs out, changes their password, or is suspended or deleted: local verification can't see those. Keep Session duration short, and call GET /v1/auth/user with the token where you need to confirm the user still exists and read their current profile. Tokens issued before ES256 signing was switched on (header alg: HS256) can't be verified locally; check those with GET /v1/auth/user until they are refreshed.

Magic Links (passwordless)

Magic links let users sign in without a password. They enter their email, receive a link, and click it to sign in. The link expires after 15 minutes and can only be used once. Email is automatically verified on first magic link sign-in.

Enable magic links in Auth → Settings → Magic Links toggle. Both email/password and magic links can be active at the same time.

Magic links only go to existing users. A magic link signs a user in; it doesn't create one. Sign the user up first (for example with eb.auth.signUp, or add them under Auth → Users). For an address with no account, requestMagicLink still returns success and sends nothing, so your app can't be used to find out which addresses are registered. The same applies to forgotPassword and resendVerification. To see what happened to a request, open Auth → Email log.
// 1. Send magic link to user's email
await eb.auth.requestMagicLink('user@example.com')

// 2. User clicks the link in their inbox
// Your app receives the token via URL: /auth/callback?token=abc123
const token = new URL(location.href).searchParams.get('token')

// 3. Exchange the token for a session
const { data, error } = await eb.auth.signInWithMagicLink(token)
// data.access_token, data.user.email — user is now signed in

How it works under the hood

  1. requestMagicLink sends a POST to /v1/auth/request-magic-link with the email
  2. The server generates a one-time token (32 random bytes), stores a SHA-256 hash in the database, and emails the raw token in a link
  3. The user clicks the link, your app extracts the token query parameter
  4. signInWithMagicLink sends the token to /v1/auth/signin-magic-link
  5. The server verifies the token (not expired, not used), marks it as consumed, and returns a JWT + refresh token

Phone Auth (SMS OTP)

Phone auth lets users sign in with their phone number instead of an email. They receive a 6-digit verification code via SMS that expires after 10 minutes. Phone numbers must be in E.164 format (e.g., +33612345678).

Enable phone auth in Auth → Settings → Phone (SMS OTP) toggle. The gateway must have GATEWAYAPI_TOKEN configured. SMS is sent via GatewayAPI, an EU-based provider (Denmark) — no data leaves EU infrastructure.

Paid plan required. SMS auth is available on Pro and above because every OTP send has a per-message cost. Free projects see the toggle grayed out; a direct API call to /v1/auth/phone/send-otp from a Free project returns 402 paid_plan_required.

// 1. Send OTP to phone
POST /v1/auth/phone/send-otp
Body: {"phone": "+33612345678"}

// 2. Verify code and get session
POST /v1/auth/phone/verify
Body: {"phone": "+33612345678", "code": "123456"}
// Returns: access_token, refresh_token, user

How it works

  1. Your app sends the phone number to /v1/auth/phone/send-otp
  2. Eurobase creates a user (if new) and sends a 6-digit code via SMS
  3. The user enters the code in your app
  4. Your app sends the phone + code to /v1/auth/phone/verify
  5. Eurobase verifies the code, confirms the phone number, and returns JWT tokens

Phone-only users: Users who sign in with only a phone number are created without an email. They can later add an email via account linking. Phone auth can coexist with email/password and social login.

Social Login (OAuth)

Eurobase supports social login with Google, GitHub, LinkedIn, and Apple. Users authenticate with their existing account at the provider — Eurobase only receives their verified email, name, and profile picture. No application data is shared with the provider, and all user records remain in EU infrastructure.

Setting up a provider

  1. Go to Auth → Settings and toggle on the provider you want
  2. Create an OAuth app on the provider's developer console (links are shown in the setup instructions)
  3. Set the redirect/callback URL to your Eurobase API URL + /v1/auth/oauth/{provider}/callback
  4. Copy the Client ID and Client Secret into the Eurobase console
  5. Add your app's URL to the Allowed redirect URLs list in Session Settings

Provider-specific notes

Google & GitHub

Standard OAuth 2.0 setup. You need a Client ID and Client Secret from their developer consoles. GitHub fetches the primary verified email if the user's email is private.

LinkedIn

Uses OpenID Connect. When creating your LinkedIn app, you must request the "Sign In with LinkedIn using OpenID Connect" product under the Products tab. Standard Client ID + Client Secret setup.

Apple

Requires additional configuration: a Service ID (used as Client ID), Team ID, Key ID, and a .p8 private key file from the Apple Developer Portal. Apple only sends the user's name on the first authorization — subsequent logins won't include it. Users may also receive a private relay email address if they choose to hide their real email.

SDK usage

// Redirect to provider's login page
eb.auth.signInWithOAuth('google', {
  redirectTo: 'https://myapp.com/auth/callback'
})
// Supported providers: 'google', 'github', 'linkedin', 'apple', 'microsoft'

// On your callback page — extract tokens from URL fragment
const { data, error } = await eb.auth.handleOAuthCallback()
// data.access_token, data.user — user is now signed in
// (SDK 0.8.1+: data.user.id / email are set right away; getUser() adds the profile)

REST API

// 1. Initiate OAuth — redirects browser to provider
GET /v1/auth/oauth/{provider}?redirect_url=https://myapp.com/callback
// 2. Provider redirects back with tokens in the URL fragment
https://myapp.com/callback#access_token=eyJ...&refresh_token=...&token_type=bearer&expires_in=604800

How it works under the hood

  1. Your app redirects the user to /v1/auth/oauth/{provider} with a redirect_url
  2. Eurobase generates a CSRF state token, encodes the redirect URL in it, and redirects the browser to the provider's consent screen
  3. The user authenticates at the provider (Google, GitHub, LinkedIn, or Apple)
  4. The provider redirects back to Eurobase's callback endpoint with an authorization code
  5. Eurobase exchanges the code for user info (email, name, avatar), finds or creates the user, and links the OAuth identity
  6. The user is redirected to your app with JWT access and refresh tokens in the URL fragment

Account linking: If a user signs up with email/password and later signs in with an OAuth provider using the same email, the accounts are automatically linked — same user ID, no duplicates. OAuth sign-in also auto-verifies the user's email.

After sign-in, the SDK automatically includes the JWT with every database and storage request. Row-Level Security (RLS) policies on your tables use this token to enforce per-user access.