dabadataDocs
Concepts / Sign in with Google, Apple and more

Concepts

Sign in with Google, Apple and more

With dabadata's hosted sign-in, your app's users can sign in with an account they already have: Google, Apple, Microsoft, GitHub, GitLab, Discord, LinkedIn or Slack. Sign-in goes through your own OAuth app at each provider, so the provider's screens show your app's name and logo, not dabadata's. The result is an ordinary hosted user with the issuer hosted: rules, the trace and live subscriptions work exactly as after a password sign-in. It needs hosted accounts turned on. For a quick start on a dabadata site, Google can also use dabadata's Google app, with no Google Cloud setup: it works on *.dabadata.site addresses only.

Which providers

ProviderWhat you needNative apps (idtoken)The email is trusted when
Google googleClient id and client secretYesGoogle says email_verified
Apple appleA Services ID only; no secretYesApple says email_verified (Hide My Email addresses too)
Microsoft microsoftApplication (client) id and client secret; tenantNoA personal Microsoft account, or a work account whose token carries xms_edov
GitHub githubClient id and client secret of an OAuth AppNoThe account's primary email is verified on GitHub
GitLab gitlabApplication id and secret, on gitlab.comNoGitLab says email_verified
Discord discordClient id and client secretNoThe Discord account is verified
LinkedIn linkedinClient id and client secretNoLinkedIn says email_verified
Slack slackClient id and client secretNoSlack says email_verified

A trusted email is what lets a provider account join an existing user. An email the provider does not vouch for signs nobody in: the answer is email_unverified (Accounts and linking).

Not yet: Facebook (its login says nothing about whether the email was verified), X (it gives no email by default), Twitch, Spotify, Bitbucket, Notion, Zoom, an OpenID Connect provider of your choice, and SAML. Until then, a provider that issues JWTs can be trusted as an issuer.

How it works

  1. The person presses "Continue with Google": on the hosted sign-in page, or a button of yours that calls db.auth.signInWithProvider("google").
  2. The browser opens https://dabadata.com/auth/<project id>/google?redirect=<your URL>. dabadata keeps a sign-in state and sends the browser on to Google with your client id.
  3. The person signs in at Google and agrees to share their name and email with your app.
  4. Google sends the browser back to the callback URL, https://dabadata.com/auth/<project id>/google/callback. dabadata redeems the code with your client secret and reads who the person is.
  5. That account is linked to a hosted user of the environment, an existing one or a new one (Accounts and linking).
  6. The browser goes back to your redirect URL with the tokens in the fragment: https://tasks.example.com/#token=...&refreshToken=...&expiresAt=...&refreshExpiresAt=.... Without a redirect, the hosted page says "You are signed in".
  7. Your page calls db.auth.completeLink(): the client takes the tokens and clears the fragment, the same as after a hosted sign-in or a mailed link.

What is checked on the way:

  • A sign-in state works once, for 10 minutes, and only in the browser that started it: an HttpOnly cookie ties the two together. A sign-in cannot be finished in another browser, or replayed.
  • The code is bound to the state with PKCE where the provider supports it (Google, Microsoft, GitHub, GitLab).
  • An ID token must name your client id as its audience, come from the provider's issuer, not be expired, and carry the state's nonce when it carries one. Apple's ID token passes through the browser, so it must carry the nonce and its signature is checked against Apple's keys too.
  • The provider's addresses are fixed in dabadata, never read from your definition.

Turn one on

In the console: open Authentication > Sign-in methods and pick the provider. The panel shows the callback URL to register at the provider, for the environment you are in. Paste the client id and the client secret and save. The console stores the secret as the project secret <PROVIDER>_CLIENT_SECRET (GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_SECRET, ...) and adds the provider to the definition. Do it per environment: test first, live when you are ready.

With the API: store the client secret as a project secret of the environment first, then name it in auth.users.providers and apply the definition. The order matters: PUT /v1/schema refuses a provider whose secret is not set (auth.users.providers.google: secret GOOGLE_CLIENT_SECRET is not set (store it with PUT /v1/project/secrets/GOOGLE_CLIENT_SECRET)).

bash
# 1. The client secret, as a project secret of this environment
curl -X PUT https://dabadata.com/v1/project/secrets/GOOGLE_CLIENT_SECRET \
  -H "Authorization: Bearer $DABADATA_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value":"<the client secret from Google>"}'

# 2. The whole definition, with auth.users.providers: definition.json is { "definition": { ... } }
curl -X PUT https://dabadata.com/v1/schema \
  -H "Authorization: Bearer $DABADATA_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d @definition.json

The auth part of that definition, with four providers:

json
"auth": {
  "users": {
    "enabled": true,
    "defaultRole": "member",
    "redirectUrls": ["http://localhost:5173/", "https://tasks.example.com/"],
    "providers": {
      "google":    { "clientId": "1234567890-abc.apps.googleusercontent.com", "secret": "GOOGLE_CLIENT_SECRET",
                     "audiences": ["1234567890-ios.apps.googleusercontent.com"] },
      "apple":     { "clientId": "com.example.tasks.web", "audiences": ["com.example.tasks"] },
      "microsoft": { "clientId": "00000000-0000-0000-0000-000000000000", "secret": "MICROSOFT_CLIENT_SECRET",
                     "tenant": "common" },
      "github":    { "clientId": "Ov23liEXAMPLE0000000", "secret": "GITHUB_CLIENT_SECRET" }
    }
  }
}
KeyMeaning
clientIdRequired. Your OAuth app's client id at the provider, 1 to 200 characters without spaces. Google's ends in .apps.googleusercontent.com; Apple's is the Services ID.
secretHosted projects. The name of the project secret that holds the client secret. Required for every provider but Apple, which has none, and Google, which without one is accepted by idtoken only (no button, not in providers()).
secretEnvThe default project of a self-hosted server only: the name of an environment variable that holds the client secret. Set one of secret and secretEnv.
audiencesGoogle and Apple only. Up to 10 more client ids that idtoken accepts: the client ids of your Google iOS or Android clients, your iOS app's bundle id. Not the clientId again.
tenantMicrosoft only. common (the default), organizations, consumers or a tenant id (which one).

Unknown providers and unknown keys are errors. Live and test have their own definitions and their own secrets. Removing a provider stops new sign-ins with it; the accounts already linked keep the link, so adding it back lets them in again.

The callback URL

Every provider asks where to send people back. That is the callback URL, one per environment and provider:

text
https://dabadata.com/auth/prj_example12345/google/callback          live
https://dabadata.com/auth/prj_example12345_test/google/callback     test

Each environment has its own callback URL. Register both at the provider, or make one OAuth app for test and one for live. Put the provider's id in place of google: apple, microsoft, github, gitlab, discord, linkedin, slack. On a self-hosted server it is <DABADATA_PUBLIC_URL>/auth/<project name>/<provider>/callback.

The steps below follow each provider's console at the time of writing. Providers move their menus around; the names of the fields stay much the same.

Google

Callback URL: https://dabadata.com/auth/<project id>/google/callback

  1. In the Google Cloud console, pick or create a project.
  2. Set up the OAuth consent screen (in newer consoles Google Auth Platform > Branding): your app's name, a support email, your logo, your home page and privacy policy. Under Authorized domains add dabadata.com and your own domain.
  3. Make the app External and publish it (In production). While it is in testing, only the test users you list can sign in.
  4. Credentials > Create credentials > OAuth client ID > Web application. Under Authorized redirect URIs add the callback URL of each environment.
  5. Copy the client id and the client secret into the console, or into clientId and the secret GOOGLE_CLIENT_SECRET.

dabadata asks for openid email profile only, scopes Google does not review. A logo may show only once Google has verified your app's branding; the name shows at once. For the Google button on your own page, or a mobile app, see Native apps and your own buttons; an iOS or Android client id goes in audiences.

Your own Google app works on every address your app runs on: your *.dabadata.site address and your own domains.

Google without your own app: dabadata's Google app

Instead of making a Google app, you can sign people in with dabadata's: Google's screen then says dabadata site, not your app's name. There is nothing to set up at Google.

  • In the console: on the Sites page, press Add Google sign-in. It turns on Google with dabadata's app and adds your sites' addresses to the redirect URLs. Or open Authentication > Sign-in methods > Google and choose dabadata's Google app.
  • In the definition: "google": { "shared": true } in auth.users.providers, with nothing else in it. There is no client id and no secret to store.
"providers": { "google": { "shared": true } }

While a project uses it, the console's Sites page says so on every site: the dabadata.site address shows "Google sign-in works", every custom domain shows "Google sign-in off here", and connecting a domain warns first.

  • The hosted sign-in page shows "Continue with Google" only when the person will go back to a *.dabadata.site address. A sign-in that would end on another address is stopped before Google, with the error shared_google_domain.
  • It works in the browser only. Native apps and Google's own button (idtoken) need your own Google app.
  • Sign-in runs through auth.dabadata.site, the address dabadata's Google app is registered with. There is no callback URL for you to register.

Apple

Callback URL: https://dabadata.com/auth/<project id>/apple/callback. Apple posts the answer back to it (a form POST); dabadata takes care of that.

  1. In your Apple Developer account, open Certificates, Identifiers & Profiles > Identifiers. You need an App ID with Sign in with Apple on: your iOS app's, or a new one.
  2. Add a Services ID (Identifiers > + > Services IDs). The description is your app's name; the identifier, say com.example.tasks.web, is the clientId.
  3. Open the Services ID, tick Sign in with Apple and press Configure. Primary App ID: the one from step 1. Domains and Subdomains: dabadata.com. Return URLs: the callback URL of each environment.
  4. Paste the Services ID in the console. There is no secret to add: dabadata checks Apple's signed ID token against Apple's public keys, so it needs no key file and no client secret.
  • Apple tells your app the person's name only the first time they sign in to it. dabadata keeps it when it creates the user.
  • A person may choose Hide My Email. You then get an address at privaterelay.appleid.com, which Apple counts as verified. To send mail to those addresses, register your sending domain with Apple's private email relay service.
  • For Sign in with Apple in your iOS app, add the app's bundle id to audiences and use idtoken.

Microsoft

Callback URL: https://dabadata.com/auth/<project id>/microsoft/callback

  1. In the Microsoft Entra admin center, open App registrations > New registration. Name: your app's name.
  2. Choose the Supported account types, and set tenant to match (table below).
  3. Redirect URI: platform Web, the callback URL. Add the other environment's under Authentication.
  4. Certificates & secrets > Client secrets > New client secret. Copy its Value, not its Secret ID. Client secrets expire: put a new one in before the date shown there.
  5. For work and school accounts: Token configuration > Add optional claim > ID > xms_edov (why: below).
  6. The Application (client) ID on the Overview is the clientId. Your logo goes under Branding & properties.
Supported account typestenant
Accounts in any organizational directory and personal Microsoft accountscommon (the default)
Accounts in any organizational directory (work and school only)organizations
Personal Microsoft accounts onlyconsumers
Accounts in this organizational directory only (one company)That directory's tenant id

Why xms_edov. A work or school account's email is set by its organization's admins, and Microsoft does not check that the address is theirs. This is known as nOAuth: whoever runs any Microsoft Entra tenant can give an account there someone else's email, and an app that matches accounts by email lets them into that person's account. So dabadata trusts a work account's email only when the token says Microsoft verified that the email's domain belongs to the account's organization: the optional claim xms_edov is true. Without it, work accounts get email_unverified. Personal Microsoft accounts (outlook.com, hotmail.com and the like) are trusted as they are.

GitHub

Callback URL: https://dabadata.com/auth/<project id>/github/callback

  1. On GitHub, Settings > Developer settings > OAuth Apps > New OAuth App. For an organization, the same under the organization's settings.
  2. Application name: your app's name. Homepage URL: your app. Authorization callback URL: the callback URL. An OAuth App holds one callback URL, so make one app for test and one for live.
  3. Generate a new client secret, and copy it with the Client ID. Upload your logo on the same page.

dabadata asks for read:user user:email and uses the account's primary email; an unverified primary email gets email_unverified.

GitLab

Callback URL: https://dabadata.com/auth/<project id>/gitlab/callback

  1. On gitlab.com, Preferences (your user settings) > Applications > Add new application. A group has its own under the group's Settings > Applications.
  2. Name: your app's name. Redirect URI: the callback URLs, one per line. Keep Confidential ticked. Scopes: openid, email and profile.
  3. Save, then copy the Application ID (the clientId) and the Secret.

gitlab.com only for now; a self-managed GitLab is not supported yet.

Discord

Callback URL: https://dabadata.com/auth/<project id>/discord/callback

  1. In the Discord Developer Portal, New Application with your app's name. The icon goes under General Information.
  2. OAuth2 > Redirects > Add Redirect: the callback URL of each environment. Save.
  3. On the same OAuth2 page, copy the Client ID, and Reset Secret to get the client secret.

dabadata asks for identify email. A Discord account whose email is not verified gets email_unverified.

LinkedIn

Callback URL: https://dabadata.com/auth/<project id>/linkedin/callback

  1. In the LinkedIn Developer portal, Create app: your app's name, your company's LinkedIn Page and your logo.
  2. Products > Sign In with LinkedIn using OpenID Connect > Request access.
  3. Auth > OAuth 2.0 settings > Authorized redirect URLs for your app: add the callback URL of each environment.
  4. On Auth, copy the Client ID and the Primary Client Secret.

Slack

Callback URL: https://dabadata.com/auth/<project id>/slack/callback

  1. At api.slack.com/apps, Create New App > From scratch: your app's name and the workspace you build it in. The icon goes under Basic Information > Display Information.
  2. OAuth & Permissions > Redirect URLs > Add New Redirect URL: the callback URL of each environment. Save URLs.
  3. On the same page, under User Token Scopes, add openid, email and profile.
  4. Basic Information > App Credentials: copy the Client ID and the Client Secret.
  5. To let people from other workspaces sign in, turn on public distribution under Manage Distribution.

What your users see

  • Your app at the provider. The sign-in and consent screens show the name and logo of your OAuth app, because it is your OAuth app. dabadata's name is not on them.
  • The domain it returns to. Some providers also show the domain the sign-in goes back to, which is dabadata.com, the host of the callback URL. Other hosted sign-in services work the same way, on their own domains. Sign-in on a domain of your own (a custom auth domain) is planned.
  • No page in between. The callback only redirects: from the provider the person goes straight back to your redirect URL, signed in.
  • Native apps that use idtoken never leave your app: the person sees the system's or the provider's own sign-in sheet, and your app hands the token over.

Native apps and your own buttons

When your app gets an ID token from Google or Apple itself - Sign in with Apple on iOS, Google Sign-In on Android or iOS, the Google Identity Services button or One Tap on a web page - hand it to dabadata with the publishable key:

bash
curl -X POST https://dabadata.com/v1/auth/users/idtoken \
  -H "X-Api-Key: pk_test_EXAMPLEexample0000000000" \
  -H "Content-Type: application/json" \
  -d '{"provider":"apple","idToken":"eyJraWQiOi...","nonce":"<the nonce your app made>","name":"Someone"}'
json
{
  "tokens": { "token": "...", "expiresAt": "...", "refreshToken": "...", "refreshExpiresAt": "..." },
  "user": { "id": "usr_k3x9m2p7q4w8z1ab", "email": "[email protected]", "name": "Someone", "role": "member",
            "emailVerified": true, "hasPassword": false, "createdAt": "2026-10-16T09:00:00.000Z" },
  "created": true
}

In the JavaScript client, db.auth.signInWithIdToken does the call and signs in like signIn. With the Google Identity Services button on your own page:

js
// <script src="https://accounts.google.com/gsi/client" async></script> on the page
google.accounts.id.initialize({
  client_id: "1234567890-abc.apps.googleusercontent.com", // the clientId in auth.users.providers.google
  callback: async (response) => {
    const user = await db.auth.signInWithIdToken({ provider: "google", idToken: response.credential });
    console.log("signed in as", user.email);
  },
});
google.accounts.id.renderButton(document.getElementById("google-button"), { theme: "outline" });
  • provider is google or apple, and it must be in auth.users.providers (else 400 provider_not_enabled). A Google entry without a secret is enough for this route.
  • The token is checked in full: its signature against the provider's public keys, the issuer, the expiry, and the audience, which must be the clientId or one of audiences. A native app's token names its own client id (your iOS app's bundle id for Apple; the iOS client id for Google Sign-In on iOS), so add it to audiences.
  • Nonce. Make a fresh random nonce for each sign-in and give it to the provider; it ties the token to that attempt. If the token carries a nonce claim, send the same nonce in the body: it must equal the claim, or its SHA-256 (hex) must. Sign in with Apple on iOS takes the SHA-256 of a nonce, so give Apple the hash and dabadata the raw nonce. A token without a nonce claim needs none.
  • name is used only when the user is created. Apple gives your app the name on the first sign-in only, so pass it then.

Sign in with Apple in a Swift app, in short:

swift
import AuthenticationServices
import CryptoKit

let rawNonce = UUID().uuidString + UUID().uuidString
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = SHA256.hash(data: Data(rawNonce.utf8)).map { String(format: "%02x", $0) }.joined()
// ... run an ASAuthorizationController; in its delegate, with the credential:
let idToken = String(data: credential.identityToken!, encoding: .utf8)!
let name = [credential.fullName?.givenName, credential.fullName?.familyName].compactMap { $0 }.joined(separator: " ")
// POST /v1/auth/users/idtoken { "provider": "apple", "idToken": idToken, "nonce": rawNonce, "name": name }

In your app

For the browser flow the JavaScript client has two methods: db.auth.providers() says which providers are on, and db.auth.signInWithProvider(provider, { redirectUrl }) leaves the page for the provider. Back on your page, db.auth.completeLink() takes the tokens, as it does for mailed links.

js
await db.auth.providers();
// { providers: ["google", "github"], page: "https://dabadata.com/auth/prj_example12345_test/" }

One HTML file with a button for each provider that is on:

html
<!doctype html>
<meta charset="utf-8">
<title>Tasks</title>

<p id="who">Loading...</p>
<div id="buttons"></div>
<button id="signout" hidden>Sign out</button>

<script src="https://dabadata.com/client/dist/dabadata.iife.js?v=5bce207b"></script>
<script>
  const db = dabadata.createClient({
    url: "https://dabadata.com",
    key: "pk_test_EXAMPLEexample0000000000", // your test publishable key
  });
  const $ = (id) => document.getElementById(id);
  const labels = { google: "Google", apple: "Apple", microsoft: "Microsoft", github: "GitHub",
                   gitlab: "GitLab", discord: "Discord", linkedin: "LinkedIn", slack: "Slack" };

  async function show(user) {
    $("who").textContent = user ? `Signed in as ${user.email}` : "Not signed in";
    $("signout").hidden = !user;
    $("buttons").replaceChildren();
    if (user) return;
    const { providers } = await db.auth.providers();
    for (const p of providers) {
      const b = document.createElement("button");
      b.textContent = `Continue with ${labels[p]}`;
      // The redirect URL must be one of auth.users.redirectUrls.
      b.onclick = () => db.auth.signInWithProvider(p, { redirectUrl: location.origin + "/" });
      $("buttons").append(b);
    }
  }

  db.auth.onAuthChange((user) => show(user));
  $("signout").onclick = () => db.auth.signOut();

  // Back from the provider with #token=... ? Finish signing in.
  db.auth.completeLink()
    .then(() => db.auth.user())
    .then(show)
    .catch((err) => { alert(err.message); db.auth.user().then(show); });
</script>
  • redirectUrl must be in redirectUrls. Left out, it is the current page's URL without its fragment, which must then be listed exactly. A ?mode=app on it changes nothing here: the tokens come in the fragment either way.
  • If something goes wrong, the person lands on the hosted sign-in page with the error and the buttons; a second try still brings them back to your redirectUrl.
  • signInWithProvider rejects with code provider_not_enabled when the provider is not in providers(), and unsupported outside a browser (in an app, use signInWithIdToken).
  • The hosted sign-in and sign-up pages show "Continue with ..." for the same providers, so a link to https://dabadata.com/auth/<project id>/?redirect=... needs no code at all.

Accounts and linking

Each sign-in with a provider ends at one hosted user of the environment. Which one:

  • Same person, same account. The first sign-in links the provider account to a user. Every later sign-in with that provider account is that user, even if the email at the provider changes; the user keeps their email.
  • A trusted email joins an existing account. When the environment already has a user with that email - signed up with a password, invited, or signed in with another provider - the provider account is linked to them. So one person can use Google, GitHub and a password for one account. The user list shows the linked providers in providers.
  • An email the provider does not vouch for signs nobody in, new or existing: email_unverified. A provider account without an email: email_missing.
  • A never-confirmed account is taken over safely. If the matching user never confirmed their address, someone may have signed up with it and a password of their own. The person who just proved the address through the provider gets the account: the password and name from that sign-up are dropped, every session of the account is signed out, and the address counts as confirmed (as with the sign-up secret).
  • New people get a new account when signup is "open": role defaultRole, the address confirmed, the name from the provider, no password. profiles and onSignUp run as for any sign-up. A password can be added later, with "forgot password" or db.auth.update({ password }).
  • Invite-only. With signup: "invite", only people who already have a user get in (you added them, or someone invited them); anyone else gets signup_closed. An invited person who signs in with a provider under the invited address accepts the invite that way.
  • Disabled users stay out: user_disabled.

Errors

The browser flow ends on the hosted sign-in page with ?error=<code>, which the page shows as a sentence. idtoken answers the code as JSON, { "error": "<code>" }.

CodeWhereWhenWhat to do
provider_cancelled?error=The person cancelled or declined at the provider.Nothing; they can try again.
provider_failed?error=Anything else on the way: the sign-in state was unknown, used, older than 10 minutes or from another browser; the provider refused the code; the token or profile did not check out. The reason is logged, never shown.Check the client id, the client secret and the registered callback URL; then try again in one browser.
email_missing?error=, idtoken 403The provider account shared no email.The person signs in another way, or adds an email at the provider.
email_unverified?error=, idtoken 403The provider does not vouch for the email: not verified there, or a Microsoft work account without xms_edov.The person verifies the email at the provider. For Microsoft, add the optional claim.
signup_closed?error=, idtoken 403No user has that email, and signup is "invite".Add or invite the person.
user_disabled?error=, idtoken 403The user is disabled.Enable them, if they should get in.
on_signup_failed?error=, idtoken 409The onSignUp row could not be inserted; no user was made.Fix the onSignUp table or values.
invalid_tokenidtoken 401The ID token's signature, issuer, audience, expiry or nonce did not check out.Check clientId, audiences and the nonce; get a fresh token.
provider_not_enabledidtoken 400; signInWithProviderThe provider is not in auth.users.providers (for signInWithProvider: its browser flow is not on, such as Google without a secret).Turn it on.
shared_google_domain?error=Google uses dabadata's Google app, and the person would go back to an address that is not *.dabadata.site, such as your own domain.Set up your own Google app for that domain.
provider_not_enabledidtoken 400Google uses dabadata's Google app, which is for the browser only.Use your own Google app for native sign-in.
validationidtoken 400The body: unknown fields, provider not google or apple, idToken over 8 KB, nonce over 200 characters, a bad name.Fix the call.
rate_limitedidtoken 429; the start URL answers 429 as textOver the limits.Wait Retry-After seconds.

The start URL /auth/<env>/<provider> answers a plain-text 404 when hosted accounts or that provider are off, and 400 for a redirect that is not in redirectUrls. With hosted accounts off, idtoken and providers answer 404 like every /v1/auth/users/ route.

Limits

WhatLimit (per environment)
Starting a sign-in, /auth/<env>/<provider>20 per IP address per 15 minutes
POST /v1/auth/users/idtoken30 per IP address per 15 minutes
A sign-in state10 minutes, used once

Every route is in the HTTP API reference.

dabadata documentation, written against API contract v2.