ESTAGE Support
IdeasMy tickets

App Users: your app's sign-ups in the CRM

Updated October 25, 2026

The complete guide: connect your app's own sign-in (Dedicated Cloud, Supabase, Firebase, Clerk, Auth0, any OpenID provider or your own code) to your CRM, so everyone who signs up becomes a contact. In one click, by asking the AI in the chat, or from Claude, Codex or Cursor over MCP.

In this guide

  1. What App Users does
  2. Before you start
  3. Four ways to set it up
  4. Step by step: Genesis Dedicated Cloud
  5. Step by step: every other sign-in
  6. Everything you can ask the AI in the chat
  7. From Claude, Codex, Cursor or ChatGPT (MCP)
  8. The bridge card
  9. Importing the accounts you already have
  10. What happens to each person
  11. Tags, lists and consent
  12. What you get in the CRM
  13. Privacy and safety
  14. For developers
  15. Questions people ask
  16. Troubleshooting

What App Users does

Your CRM learns about people from your forms, orders and bookings. An app with its own sign-in is different: people create an account in the app, never fill in an Estage form, and the CRM never hears of them. No list, broadcast, autoresponder or automation can reach them.

App Users is a bridge from that sign-in to the CRM:

  • Every new account becomes a contact the moment it's created, with the tags you choose.
  • The accounts you already have are imported in one go, without welcome emails.
  • The contact stays linked to the account: a changed email moves with it, a sign-in updates "last signed in", a deleted account is marked, and with one setting the person stops getting your emails.
  • A new automation trigger, Signed up in app, starts an onboarding sequence for each new account. See App sign-ups in the CRM.

Your app's sign-in stays exactly as it is. Nobody signs in through Estage, nothing changes for your users, and a sign-up is never slowed down or blocked by the bridge.

A typical example. A SaaS built in Genesis runs on a Dedicated Cloud, where people sign up with an email code or Google. Connected once, its 11 existing users land in the CRM tagged App users. From then on every new sign-up arrives within a second, and a Signed up in app automation sends them a welcome email and a tip two days later.

Before you start

Where Genesis → your project → Settings → App Users (in the AI & Automation group). Or search Settings for "App Users".
Who Someone with edit access to the project and the App Users permission in their role. The permission is marked sensitive, so give it only to people you trust with your users' data.
Which apps Any app of the project: a Website, a Web app or a Mobile app. The contacts go to the project's CRM.
Which CRM The project's own CRM at crm.estage.com. Nothing to set up there.
Cost No extra charge. Setting it up with the AI uses ordinary AI credits for that chat turn.

Four ways to set it up

Way Best when
Connect to CRM in Settings → App Users You want it done now, with a click. Works for every sign-in.
Set it up with AI in Settings → App Users The app's code needs a line added (Firebase, OpenID), or you'd rather not click through anything. The button opens the builder chat with the request already written.
Ask in the builder chat, in your own words You're already talking to the AI, for example while building a sign-up page.
From an outside AI tool over MCP You build with Claude, Claude Code, Codex, Cursor or ChatGPT connected to Genesis.

All four do exactly the same thing underneath and give the same result.

Tip

You often don't have to start it at all. When the AI builds or changes a sign-up or sign-in in your app, it checks whether those people reach your CRM and, if not, offers the bridge in one sentence. Say yes and it's done.

Step by step: Genesis Dedicated Cloud

This is the most common case: a Genesis app whose sign-in runs on its Dedicated Cloud (email and password, email codes, magic links, Google or any other provider the cloud has turned on). There is nothing to do by hand: no SQL, no code, no keys.

  1. Open Settings → App Users. Genesis has already looked at the project. A card at the top says which sign-in it found and how many accounts it has, with the evidence underneath, for example "Dedicated Cloud is active" and "app/login/LoginForm.jsx signs people in with supabase.auth".
  2. Press Connect to CRM. A short dialog opens:
    • New sign-ups: "A trigger in your Dedicated Cloud reports every sign-up, profile change and deletion the moment it happens. Nothing changes in your app." Nothing to choose.
    • Existing accounts: Import the N accounts it already has, on by default. They get no welcome emails.
    • Tag them as: App users by default. Type your own, comma-separated (for example App users, StoreReady). Missing tags are created.
    • When someone deletes their account in the app: Keep the contact as it is (default) or Keep the contact, stop emailing them.
  3. Press Connect. In a few seconds the dialog says what was done: the trigger is installed and the import has started. Press Done.
  4. Check the card. Under Connected there is now a card Genesis Dedicated Cloud with Live, the number of contacts, and the last import ("11 new contacts, 0 linked to existing ones").
  5. Try it. Sign up in your app with a new email. Within seconds it shows in the card's Activity and as a contact in the CRM, with your tags.

Instead of steps 2 and 3 you can press Set it up with AI: the builder chat opens and the AI does the same and tells you what it did.

What Genesis did for you: it added a trigger on the cloud's sign-in table through a regular, journaled migration (you can see it in Cloud → History), and wrote the bridge's address and secret straight into the database, outside the journal, so the secret never shows up in your history. Disconnecting removes both again.

Note

A linked app that uses another project's Dedicated Cloud belongs to that project's CRM. App Users says "This app shares the Dedicated Cloud of project …" and sends you there instead of making a second bridge.

Step by step: every other sign-in

If Genesis didn't find your sign-in, or found several, use Connect another sign-in at the bottom of App Users and pick it. Others it found are listed under Also found in your app, each with its own Connect.

Supabase (a project of your own)

What Genesis does depends on what it can reach:

The Supabase connector with a management token (Settings → Connectors → Supabase): the same as the Dedicated Cloud. Connect to CRM installs the trigger through the connector and imports the existing users with the keys the connector holds. Nothing else to do.

The connector without a management token, or a Supabase Genesis found in your app's code: Genesis knows the project's public address and key, which is enough to check your users' sign-ins.

  1. Press Connect to CRM, then Connect. Genesis adds the identify module to your app and shows the one line to add after createClient(…): bridgeSupabaseAuth(supabase).
  2. Press Ask AI to add it, then Publish the app.
  3. For the existing users, Import existing takes the Project URL and the Secret (service_role) key, or a CSV of auth.users.

For the instant database trigger instead, add a management token to the Supabase connector and connect again, or run the SQL from Set up by hand once.

Nothing Genesis can reach (a Supabase it doesn't know about):

  1. Press Connect to CRM, then Connect. The result shows the SQL with the bridge's secret already filled in. Copy it now: the secret is shown once.
  2. Open your project in the Supabase dashboard → SQL Editor, paste, Run. The script enables pg_net, creates a private schema estage_bridge and a trigger on auth.users, and sends a test delivery.
  3. The card turns Live when the test delivery arrives.
  4. For the import, Import existing takes the Project URL and the Secret (service_role) key, or a CSV exported from Supabase.

Later, Set up by hand on the card shows the same SQL (and the script that removes it). Running it again is safe. If you've lost the secret, Make a new secret there fills it in. Then run the SQL again, because the old secret stops working at once.

Tip

One Supabase database can serve the apps of several Estage projects. Each project connects its own bridge, and every sign-up then reaches every one of their CRMs. Removing one project's bridge leaves the others working.

Firebase Authentication

Firebase has no database trigger, so the app hands the signed-in person's ID token to Estage, which checks it with Google before filing anyone.

  1. Press Connect to CRM. If Firebase isn't in Connectors, fill in the Firebase project id (Firebase console → Project settings, for example my-app-1a2b3). Genesis fills it in when it finds it in your app's config.
  2. Press Connect. Genesis adds a small protected module to your app, src/lib/estageIdentity.js (lib/estageIdentity.js in a Web app), and shows the one line to add next to getAuth(app): bridgeFirebaseAuth(auth).
  3. Press Ask AI to add it and the AI adds the line. Or ask in the chat yourself.
  4. Publish your app, so the line reaches your users.
  5. For the existing accounts, Import existing takes the Service account key (JSON) (Firebase console → Project settings → Service accounts → Generate new private key; paste it or drop the file), or a firebase auth:export file.

Optional: tick Also check for new accounts every 10 minutes. With the service account key it picks up new accounts even before the line in your app is published. If you're on Firebase's Blaze plan, Set up by hand also has Cloud Functions that report sign-ups and deletions the moment they happen.

A token counts as a sign-up only when the account is less than 48 hours old. An older account's first token links the person without starting your Signed up in app automations.

Clerk

  1. Press Connect to CRM, then Connect. The result shows the bridge's events address.
  2. In the Clerk dashboard: Webhooks → Add endpoint, paste the address and subscribe to user.created, user.updated, user.deleted and session.created.
  3. Copy the endpoint's Signing secret (whsec_…) and paste it in the card's Settings → Clerk webhook signing secret. Until then the card says Waiting for Clerk's webhook signing secret.
  4. For the import, Import existing takes Clerk's Secret key (sk_live_…), or a CSV export.

Auth0

  1. Press Connect to CRM, then Connect. Copy the secret shown in the result, along with the ready Action code.
  2. In Auth0: Actions → Library → Build Custom, trigger Post User Registration.
  3. Add a secret named ESTAGE_BRIDGE_SECRET with the bridge's secret, paste the code, Deploy, and add the Action to the Post User Registration flow.
  4. Existing users: export them from Auth0 and upload the file in Import existing.

OpenID Connect (Cognito, Kinde, Keycloak and others)

  1. Press Connect to CRM. Fill in the Issuer URL (for example https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_AbC123) and, if your tokens carry one, the Audience (the client id they are issued for).
  2. Press Connect. Genesis adds the identify module and the AI adds one line after sign-in: identifyWithEstage(idToken, { userId }).
  3. Existing users: upload an export (CSV or JSON) in Import existing.

Estage checks each token against the issuer's published keys. A token from another app fails that check.

Your own sign-in

When your app signs people in with its own code and database:

  1. Connect another sign-in → Your own sign-in → Connect. Copy the secret from the result.
  2. Your server makes one call when someone signs up, changes their profile, signs in or deletes their account. Set up by hand has ready code: a curl call and a Node.js function that signs each request. The format is under For developers.
  3. Existing users: your server sends them in batches of up to 500 ({ "users": [ … ] }), or you upload a CSV with an email column.

Or ask the AI: "Report every sign-up in my app to my CRM with the Auth Bridge." It wires the calls on the server side and keeps the secret out of the browser.

Which sign-in needs what: summary

Sign-in New accounts arrive by Import existing accounts with You do
Genesis Dedicated Cloud A database trigger, instantly Nothing, automatic Nothing
Supabase, connector with a management token A database trigger, instantly Nothing, automatic Nothing
Supabase, connector without the token, or found in the code The app hands over the token Project URL + secret key, or a CSV One line (the AI adds it), then publish
Supabase Genesis can't reach A database trigger, instantly Project URL + secret key, or a CSV Run one SQL script once
Firebase The app hands over the token; optionally a 10-minute check Service account JSON, or firebase auth:export One line (the AI adds it), then publish
Clerk Clerk's webhook, instantly Secret key, or a CSV Add the endpoint, paste the signing secret
Auth0 An Action, instantly An export file Paste the Action
OpenID Connect The app hands over the token An export file Issuer URL; one line (the AI adds it)
Your own sign-in Your server's call { "users": [ … ] }, or a CSV One call in your server code

Everything you can ask the AI in the chat

The builder's AI has the bridge as its own tools. Ask in plain words, in any language. It checks first, does the work, and tells you what's done and what (if anything) is left for you.

Find out where you stand

  • "Who signs in to my app, and are they in my CRM?"
  • "Is the CRM bridge working? When did the last sign-up arrive?"
  • "How many of my app's users are contacts?"

Connect

  • "Connect my app's sign-in to my CRM and import the existing users."
  • "Connect it, tag everyone as Beta testers, and don't import the old users."
  • "Connect Firebase. The project id is my-app-1a2b3."
  • "Connect our Cognito sign-in. The issuer is https://cognito-idp…"

Build and connect at once

  • "Add sign-up with email and Google, and put everyone who signs up in my CRM."
  • "Make a members area with sign-in, and add new members to my Newsletter list when they agree to emails."

Import

  • "Import my app's users into the CRM again."
  • "Import them and run my new-contact automations for them too."

Tags, lists and consent

  • "Tag everyone from the app as App users and StoreReady."
  • "Add new sign-ups to the Newsletter list."
  • "Only add them to the list when they ticked marketing emails. That's saved in user_metadata.marketing_opt_in."

When someone deletes their account

  • "When someone deletes their account in the app, stop emailing them."
  • "Keep emailing people who delete their app account." (back to keep)

Pause, fix, remove

  • "Pause the CRM bridge for now." / "Resume it."
  • "The bridge says Needs attention. Fix it."
  • "New sign-ups stopped arriving in the CRM. Check it."
  • "Disconnect the CRM bridge." (the contacts stay)

Clerk

  • "Here's Clerk's webhook signing secret: whsec_…" The AI saves it on the bridge.

What the AI will not do, on purpose:

  • It never asks you to paste a service key, a service account or a secret key into the chat. Keys for an import are typed in Settings → App Users, where they're used once and erased. The only key it accepts in the chat is Clerk's webhook signing secret, which can't read anything.
  • It never writes its own code to send people to Estage, and never puts a key in your app's code. It uses the bridge's module or trigger.
  • It connects, changes or disconnects only after you've said so. Checking is free to do anytime.

Note

When you press Set it up with AI or another … with AI button in Settings, the builder opens and the request appears in the chat as soon as the chat has loaded, with the AI's work showing live. If you reload the page while the AI is working, the chat picks the running turn back up and keeps showing it.

From Claude, Codex, Cursor or ChatGPT (MCP)

Any AI tool connected to Genesis over MCP gets the same bridge, as five tools. Set up the connection once, as in Connect Claude (Codex, Cursor and ChatGPT work the same way), then ask in your own words, exactly as in the chat above.

Both kinds of connection have the tools:

  • One project: the address https://genesis.estage.com/api/agent/<project id>/mcp.
  • Your whole account: every tool takes a projectId, and the AI finds the project you name.

The tools

Tool What it does Needs
genesis_auth_bridge_status Which sign-in the app uses and how many accounts it has, the bridges already connected with their health (live, waiting, error, paused), contacts, last import, and a suggestion when people sign in but nothing brings them to the CRM. Read-only. fresh: true looks at the project again instead of the minute-old answer. CRM read
genesis_auth_bridge_connect Connects the sign-in and imports existing accounts. Installs the trigger itself on a Dedicated Cloud or a Supabase connector with a management token; otherwise adds the identify module and returns the one line to add (wiring), or the webhook steps. CRM write, plus Cloud write for a Dedicated Cloud, Connectors admin for Supabase
genesis_auth_bridge_import Imports (again) the accounts a connected sign-in already has. Nobody is added twice; no automation runs unless asked. CRM write
genesis_auth_bridge_update Pause or resume, tags, lists, consent, what a deleted account means, the 10-minute check, Clerk's signing secret. CRM write
genesis_auth_bridge_disconnect Stops the bridge and removes the trigger. The contacts stay. CRM write

Their parameters

genesis_auth_bridge_connect

Parameter
provider dedicated_cloud, supabase, firebase, clerk, auth0, oidc or custom. Leave it out to use the sign-in Genesis detected.
importExisting Import the existing accounts. Default true.
tagNames Tags for everyone it brings in, created when missing. Default ["App users"]; [] for none.
listIds CRM lists for new sign-ups.
consentPath Where the profile keeps marketing consent, e.g. user_metadata.marketing_opt_in. Lists then only for people who said yes.
onDelete keep or suppress.
poll Check for new accounts every 10 minutes (Firebase).
issuer, audience For OpenID Connect, Auth0 or Clerk tokens.
firebaseProjectId Firebase without a connector.

genesis_auth_bridge_update: sourceId (from status), plus any of status (active or paused), tagNames, tagIds, listIds, consentPath, onDelete, poll, providerSecret (Clerk's whsec_…).

genesis_auth_bridge_import: sourceId, and runAutomations (default false).

genesis_auth_bridge_disconnect: sourceId.

The connect answer has ok, message (what to tell you), live (how new accounts will arrive and whether that's done), import (whether it started and how many), wiring (the line the AI adds to your app, when there is one) and next (anything left for you). An agent never receives a secret or a provider key in any of them.

A typical conversation

You: Are my app's users in my CRM?

Claude (calls genesis_auth_bridge_status): Your app signs people in with its Dedicated Cloud (11 accounts), and nothing brings them to your CRM yet. Shall I connect it and import them?

You: Yes, tag them StoreReady.

Claude (calls genesis_auth_bridge_connect with tagNames: ["StoreReady"]): Done. Every new sign-up becomes a contact the moment it happens, and the 11 existing users are being imported, without welcome emails.

The bridge card

Every connected sign-in has a card under Connected in Settings → App Users.

The status

Status Means
Live Deliveries arrive. Nothing to do.
Waiting for the first sign-up Connected, nothing has arrived yet. Normal right after connecting a sign-in that has no trigger: sign up once to see it work.
Needs attention The last delivery failed. The card shows the reason and Fix with AI.
Paused You paused it. New accounts are not filed until you resume.

What it shows: how it syncs ("A database trigger reports every sign-up, change and deletion"), the number of contacts it brought in, what arrived in the last 24 hours, when the last delivery came, and the last import (new contacts, linked to existing ones, skipped and why). While an import runs, a progress bar.

Its buttons

Button
Import existing Imports the accounts the sign-in already has, or re-imports them. Keys or an export file where the sign-in needs them (never for a Dedicated Cloud). Optional: Run my "new contact" automations for them.
Activity The last deliveries: when, what (created, updated, signed in, deleted), the email, and whether it was filed or skipped and why.
Set up with AI Opens the builder chat with a request to check the bridge and finish or repair whatever it still needs.
Set up by hand Only for sign-ins outside Genesis: the steps, the events and identify addresses, the code (SQL, Action, curl, Node.js) and Make a new secret. Not shown for a Dedicated Cloud, where there's nothing to do by hand.
Settings Also tag people it brings in; When someone deletes their account in the app (keep, or keep and stop emailing); Clerk webhook signing secret for Clerk.
Pause / Resume Stops and restarts filing new accounts.
Disconnect Stops the bridge after a confirmation. The trigger Genesis installed is removed and the module goes quiet. Every contact it brought stays in the CRM.

Importing the accounts you already have

An import brings in every account the sign-in already has, as contacts linked to their accounts.

  • Automatic for a Dedicated Cloud, a Supabase connector with a management token, and a Firebase connector: Genesis holds the keys.
  • With keys you type once in Import existing: Supabase (Project URL and Secret (service_role) key), Firebase (Service account key, JSON), Clerk (Secret key). They're used for that import only and erased when it ends. They're never stored and never shown to the AI.
  • With an export file, from anything:
File
firebase auth:export JSON, as the Firebase CLI writes it
Supabase CSV of the auth.users table
Clerk CSV export
Any CSV Needs an email column. Optional: id (or uid, user_id), name (or full_name, display_name), first_name, last_name, phone, created_at, last_sign_in_at

Up to 50 MB per file and 200,000 accounts per file (a JSON file up to 20 MB; export CSV for more, or split it).

What an import does

  • Runs in the background. You can close the page; the card and the CRM show the progress.
  • Starts no automations by default: no welcome email, no Contact created, no Tag added, no Signed up in app. These people signed up long ago. Tick Run my "new contact" automations for them only if you mean it.
  • Adds nobody twice. Run it again anytime: people already linked are updated, not copied.
  • Ends with a line: "Last import: 11 new contacts, 0 linked to existing ones · 2 skipped (with no email address)".

Why an account can be skipped

Reason
no email address Phone-only accounts. The CRM is keyed by email.
a role address postmaster@, abuse@ and similar, which are never a person
an invalid address Not a deliverable email
an unconfirmed address Only when the bridge is set to require confirmed emails
an unreadable row A line in a file that couldn't be read
the bridge was paused Deliveries that arrived while paused
a repeated delivery The same event sent twice, filed once

What happens to each person

  • One account, one contact. The account is recognised by its id in the sign-in, the person by their email. A changed email in the app moves the link to the new address instead of making a second contact.
  • Nothing you know is overwritten. Someone already in your CRM, from a form or an order, is linked to their account and keeps their name, tags, lists and history. A missing phone number is filled in, an existing one is kept.
  • Tags on arrival. The bridge's tags go on the first time a person arrives through it.
  • New sign-ups start automations: Signed up in app, and your other new-contact automations. Imports don't, unless you ask.
  • Sign-ins update last signed in, at most once an hour per person.
  • A deleted account keeps the contact and marks the account Removed in app. With stop emailing them (suppress), the address is also suppressed and unsubscribed from the project's lists.
  • A contact you delete from the CRM isn't brought back by their next sign-in.
  • The same email in different projects gives each project its own contact, with its own tags, lists, history, automations and unsubscribes. One project's bridge never changes another project's contacts. The person's shared Estage account details (name, phone) are never rewritten by a bridge.
  • Tags are set when connecting (Tag them as) and changed in the card's Settings (Also tag people it brings in) or by asking the AI.
  • Lists are added by asking the AI ("Add new sign-ups to the Newsletter list"). They apply to new sign-ups, and the bridge never adds someone to a list over an unsubscribe: a suppressed email, or anyone who left that list, is left alone.
  • Consent. If your sign-up form asks for permission to email, tell the AI where it's saved (for example user_metadata.marketing_opt_in). Then only people who said yes are added to your lists. Tags still go on everyone, since tags don't send anything.

What you get in the CRM

Everything below is in the project's CRM at crm.estage.com. The full guide is App sign-ups in the CRM.

  • Contacts with your tags, ready for lists, broadcasts, segments and automations.
  • App accounts on each contact's page: which sign-in, when they signed up and how (email, Google…), when they last signed in, Removed in app.
  • The contact's Automation Log: Signed up in app, App account linked, App account deleted.
  • The Signed up in app automation trigger, for every sign-in or only the ones you pick.
  • Settings → Import → Your app's users: each connected sign-in, its health, and an import of its accounts with keys or a file.

Privacy and safety

  • Your users' sign-in is untouched. Estage never signs anyone in, sees no password and holds no session.
  • Only what a CRM needs is sent: email, whether it's confirmed, name, phone, when the account was created and last used, how the person signs in, and the account's id. Never a password, a password hash, a token or session data. The database trigger reads only those columns.
  • A sign-up is never blocked. If the CRM can't be reached, the trigger and the module give up quietly and the app carries on.
  • Tokens your app hands over are checked with the provider that issued them, used once, and never stored or logged. A token from another app is refused.
  • Secrets are shown once, when they're made, and stored encrypted. Keys for an import are used for that import and erased.
  • The AI never sees a secret or a provider key, in the chat or over MCP.
  • Every delivery is authenticated (the bridge's secret, a signature, or Clerk's signed webhook) and rate-limited. An unsigned request is refused.
  • Who can touch it is decided by the App Users permission and the project role. The AI and MCP tools follow the same rules.

For developers

The events endpoint

POST https://api.estage.com/api/public/identity/<source id>/events

The source id (isrc_…) and the full address are in the card's Set up by hand.

Authentication, one of:

  1. Authorization: Bearer <secret>, the simplest, for a trigger or a no-code tool.
  2. A signature, for your own server: X-Estage-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the secret>. Accepted within 5 minutes of t.
  3. Standard Webhooks (Svix) headers signed with the provider's secret. That's how Clerk signs.

Body

{
  "type": "user.created",
  "id": "evt_123",
  "user": {
    "id": "42",
    "email": "almira@example.com",
    "email_verified": true,
    "name": "Almira Costa",
    "first_name": "Almira",
    "last_name": "Costa",
    "phone": "+15551234567",
    "created_at": "2026-10-26T09:00:00Z",
    "last_sign_in_at": "2026-10-26T09:00:00Z",
    "method": "google",
    "metadata": { "plan": "pro" }
  }
}
  • type: user.created, user.updated, user.signed_in, user.deleted (and user.imported).
  • id: the event's id. A retry with the same id is filed once. The headers X-Estage-Event-Id, webhook-id and svix-id work too.
  • Several at once: { "events": [ … ] }, up to 500.
  • Existing accounts: { "users": [ … ] }, up to 500 per call. Treated as an import: no automations, no Activity entries.
  • Supabase Database Webhook bodies (INSERT / UPDATE / DELETE on auth.users) and Clerk webhook bodies are understood as they are.

Answer: 200 { "accepted": 1, "skipped": 0, "failed": 0, "ignored": 0 }. Events that aren't about accounts (such as organization.created) are ignored, never an error, so the sender doesn't retry them. Limit: 1,200 deliveries a minute per bridge.

Node.js, signed

import crypto from 'node:crypto'

// Call when someone signs up (user.created), changes their profile
// (user.updated), signs in (user.signed_in) or deletes their account (user.deleted).
export async function reportToEstage(type, user) {
  const body = JSON.stringify({
    type,
    id: crypto.randomUUID(),
    user: { id: String(user.id), email: user.email, name: user.name, created_at: user.createdAt },
  })
  const t = Math.floor(Date.now() / 1000)
  const v1 = crypto.createHmac('sha256', process.env.ESTAGE_BRIDGE_SECRET).update(`${t}.${body}`).digest('hex')
  await fetch('https://api.estage.com/api/public/identity/<source id>/events', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Estage-Signature': `t=${t},v1=${v1}` },
    body,
  })
}

curl

curl -X POST 'https://api.estage.com/api/public/identity/<source id>/events' \
  -H 'Authorization: Bearer <secret>' \
  -H 'Content-Type: application/json' \
  -d '{"type":"user.created","id":"evt_1","user":{"id":"42","email":"almira@example.com"}}'

Keep the secret on your server, in an environment variable such as ESTAGE_BRIDGE_SECRET. Never put it in browser or mobile code.

The identify endpoint (tokens from the browser)

POST https://api.estage.com/api/public/identity/<source id>/identify
{ "token": "<the signed-in person's ID or access token>", "created_at": "<optional>" }

Open to browsers (CORS). The token is checked with its issuer: Supabase's /auth/v1/user, Google's keys for Firebase, the issuer's JWKS for OpenID Connect. The answer is 200 { "ok": true } and nothing else; it never reveals a contact. Errors: 401 with invalid_token, wrong_audience or token_expired; 502 verifier_unavailable. Limits: 30 a minute per IP, 1,200 per bridge.

The identify module

For apps that hand tokens over, Genesis adds src/lib/estageIdentity.js (lib/estageIdentity.js in a Web app). It's protected: Genesis rewrites it when the bridge changes, so don't edit it.

import { bridgeSupabaseAuth, bridgeFirebaseAuth, identifyWithEstage } from '@/lib/estageIdentity'

bridgeSupabaseAuth(supabase)          // where the Supabase client is made
bridgeFirebaseAuth(auth)              // next to getAuth(app)
identifyWithEstage(idToken, { userId, createdAt })  // any other sign-in, after it succeeds

It never throws, never blocks the sign-in, and sends each person at most once per browser every 12 hours. It holds no secret: only the bridge's public identify address.

The database trigger (Supabase and Dedicated Cloud)

  • An AFTER INSERT OR UPDATE OR DELETE trigger on auth.users, in a private schema estage_bridge that anon and authenticated can't read, using pg_net.
  • Sends only the columns above. An update is sent only when something the CRM keeps changed; a sign-in at most once an hour per person.
  • Swallows every error: a CRM that's down never blocks a sign-up.
  • One row per connected Estage project in estage_bridge.endpoints, so one database can report to several projects; an unreachable one doesn't stop the others.
  • The uninstall script removes this project's row; the trigger and schema go with the last one.

Questions people ask

Does my app change? Do my users notice anything? No. They sign up and sign in exactly as before. On a Dedicated Cloud or a Supabase with the connector's management token, not a line of your app changes. Otherwise one line is added after sign-in.

Do my existing users get a welcome email when I import them? No, unless you tick Run my "new contact" automations for them.

Someone signed up with an email that's already a contact from my form. Do I get a duplicate? No. The existing contact is linked to the account and keeps everything it had.

The same person uses two of my projects. Each project's CRM gets its own contact. Tags, lists, unsubscribes and automations are per project.

Can I connect two sign-ins to one project? Yes, for example a website on Firebase and a mobile app on Supabase. Each has its own card, and the Signed up in app trigger can tell them apart.

What if I disconnect? New sign-ups stop arriving. Every contact stays. You can connect again anytime; nobody is added twice.

Someone deleted their account in my app. Are they still emailed? By default the contact stays as it is. Choose Keep the contact, stop emailing them to unsubscribe them automatically.

Phone-only accounts? They're skipped, because the CRM is keyed by email.

Is my users' data safe? Only CRM fields are sent, deliveries are signed, tokens are checked and never stored, and secrets are shown once and stored encrypted. See Privacy and safety.

Troubleshooting

You see What to do
Waiting for the first sign-up Normal right after connecting. Sign up once with a new email and watch Activity. Where a line was added to your app (Firebase, OpenID, some Supabase), check it's there and the app is published.
Needs attention Read the reason on the card and press Fix with AI.
Waiting for Clerk's webhook signing secret Paste the endpoint's whsec_… in the card's Settings, or give it to the AI.
Sign-ups stopped arriving Is the card Paused? Otherwise ask the AI: "New sign-ups stopped reaching my CRM, check the bridge."
Contacts missing after an import The import line says how many were skipped and why: usually no email or a role address.
An import says it needs keys Type them in Import existing, or upload an export file.
This app shares the Dedicated Cloud of project … Connect the bridge in that project's App Users. Its CRM gets the sign-ups.
You lost a bridge's secret Set up by hand → Make a new secret, then update the place that uses it (SQL, Action, server). The old one stops working at once.
The AI started but the chat looks idle Wait a moment, or reload. The chat picks a running AI turn back up and shows it.
No App Users in Settings You need the App Users permission in your role. Ask the project owner.
Was this helpful? Still stuck? Write to the team