One Login Across Seven Websites for $0: How to Add Google Sign-In and Stripe Payments with Clerk and Cloudflare D1 (Clerk + Cloudflare D1 + Stripe)

 


How to Add Google Sign-In and Stripe Payments to Your Web App with Clerk and Cloudflare: The Exact Steps

A practical guide to authentication, Cloudflare D1, Stripe and shared logins across subdomains, with lessons from building accounts for my free party game: One Login Across Seven Websites, for $0:

Stripe + Clerk + Cloudflare D1 + Google sign-in, click by click — including the five traps that waste an afternoon


What you’ll learn

- How to connect Clerk, Google sign-in, and Cloudflare D1.
- How to keep account records and purchases in sync
- How authentication, customer records, and paid access fit together.
- Why your domain structure matters for shared logins.
- How to test the experience beyond a successful login screen.
- Where free plans help — and where their limits begin.

I run seven country editions of a browser game, each on its own website. I needed one account that works on all of them, a record of who signed up and who paid, and a monthly bill of zero.

This is the build, in the order you should do it. Every value you need to type or paste is here. Substitute yourgame.com for your own domain throughout.

What you’ll have at the end: sign-in with email or Google on every site, one account shared across all of them, a database recording signups and purchases, and refunds that automatically take back whatever the purchase unlocked.

Cost: free to 50,000 monthly users (Clerk), free database (Cloudflare D1), free Google sign-in.

Before you start: your sites hosted on Cloudflare Pages, your domain’s DNS on Cloudflare, and a Google account.

Step 1 — Put every site on one domain

Do this first. Auth providers share a login across subdomains for free. Across different domains it’s a paid feature — for Clerk, Pro plus $10/month per extra domain. Seven sites on seven domains would have cost me $100+/month for something that’s otherwise free.

For each site:

  1. Cloudflare dashboard → Workers & Pages → click your project
  2. Custom domains tab → Set up a custom domain
  3. Type the subdomain: ke.yourgame.com → Continue
  4. Cloudflare shows the CNAME it will create → Activate domain
Confirm new DNS record

Repeat for each site: ke., gh., eg., et. and so on. The old *.pages.dev addresses keep working.

Give it two minutes for the certificate, then load each address over https.

Don’t miss this: if you have a hub or landing page linking to your other sites, update those links to the new subdomains now. Anyone arriving at a site via an old pages.dev link is on a different domain and will appear signed out — which defeats the entire exercise. I had seven of these links and nearly shipped them.

Step 2 — Set up Clerk

2a. Create the application

  1. dashboard.clerk.com → create application
  2. Project type: Consumer (B2B is for teams with roles)
  3. Name it — this appears in the sign-in box, so use your real product name
  4. Sign-in options: turn on Email and Google. Turn off phone (every SMS costs money) and username
  5. Create application
he project type and sign-in options screen

You’re now in a Development instance, which only works on localhost. Ignore the framework setup wizard it shows you — that’s for Next.js/React apps.

2b. Create the production instance

Click Go to prod (top right) → choose Clone development instance → Continue.

Create production instance

Cloning carries over the sign-in options you just picked. “Create default instance” makes you redo them.

2c. Add your domain

When asked for the domain, enter the apex:

Plaintext

yourgame.com

Not www., not a subdomain. The apex is what makes the login work across every subdomain for free.

the Domains page showing the domain as “Unverified”, and the “Satellites” tab marked Pro

That “Satellites — Pro” tab is the paid cross-domain feature you’re avoiding by doing Step 1.

2d. Add the DNS records

Configure → Developers → Domains → click Configure next to your domain. You get about five CNAME records:

  • Name: clerk | Points to: frontend-api.clerk.services
  • Name: accounts | Points to: accounts.clerk.services
  • Name: clkmail | Points to: mail.<id>.clerk.services
  • Name: clk._domainkey | Points to: dkim1.<id>.clerk.services
  • Name: clk2._domainkey | Points to: dkim2.<id>.clerk.services
the Clerk page listing the Frontend API and Account portal records

The first two are the login itself; the last three let Clerk send verification emails from your domain.

Easiest route: click Configure automatically at the top. Clerk connects to Cloudflare and adds all five itself, with the correct proxy setting. Review the confirmation screen and click Authorize.

the Cloudflare authorization screen listing all five records as “DNS only”

Adding them by hand? In Cloudflare → DNS → Add record, set type CNAME, name exactly as shown (Cloudflare appends your domain), and — critically — Proxy status: DNS only, the grey cloud. Proxied records make verification fail forever with no useful error.

Then click Verify Records in Clerk. Frontend API and Account portal verify first, email follows, SSL certificates come last.

2e. Get your keys

Instance → API keys (bottom of the left sidebar — not Developers → API keys, which is a different feature).

the API keys page showing the publishable key and the secret key

Copy both:

  • Publishable key (pk_live_…) — public by design, goes in your website code
  • Secret key (sk_live_…) — master access to every account. Dashboard only, never in a file, never in a screenshot

Step 3 — Configure Google sign-in

Production instances can’t use Clerk’s shared test credentials, so you need your own free Google OAuth app.

3a. Get the redirect URI from Clerk

Configure → SSO connections → click Google (“Setup required”). Copy the Authorized Redirect URI:

Plaintext

https://clerk.yourgame.com/v1/oauth_callback
the Google OAuth page in Clerk showing the redirect URI and the empty Client ID / Client Secret fields

Leave that tab open.

3b. Create a Google Cloud project

Go to console.cloud.google.com/projectcreate → name it → Create. Check the project chip at the top switched to your new project before continuing.

No billing account is needed. OAuth sign-in is free.

3c. Fill in the consent screen

console.cloud.google.com/auth/overview → Get started:

  • App name: your product name
  • User support email: your address
  • Audience: External
  • Developer contact: your address

Then Branding, where you fill in:

Setting Up Your Game’s Domain & Legal Links
  • Application home page: [https://yourgame.com]
  • Privacy policy link: [https://yourgame.com/privacy.html]
  • Terms of service link: [https://yourgame.com/terms.html]
  • Authorized domains → Add domain: yourgame.com (bare, no https://)
the Branding page with the app domain fields and Authorized domains

Do not upload a logo. Despite what my screenshot above says. 
A logo triggers Google’s brand verification: days of review that can bounce back. Without one, and asking only for name and email, you publish instantly.

3d. Publish it

console.cloud.google.com/auth/audience → Publish app → Confirm.

[IMAGE: Screenshot from 2026-09-24 11-55-40.png — the "Push to production?" dialog]

Leave it in “Testing” and only email addresses you manually list can sign in, capped at 100.

3e. Create the client and paste it back

console.cloud.google.com/auth/clients → Create client:

  • Application type: Web application
  • Name: Clerk
  • Authorized redirect URIs → Add URI → paste the URI from 3a, exactly, no trailing slash
  • Create

Copy the Client ID and Client secret into the Clerk tab → Save.

⚠️ Google’s confirmation dialog displays the secret in full. Don’t screenshot it. If you already did, rotate it afterwards: Clients → your client → add a new secret → update Clerk → delete the old one.

Step 4 — Create the database

4a. Create it

Cloudflare → Storage & Databases → D1 SQL Database → Create:

  • Name: yourgame
  • Data location: expand Provide a location hint and pick the region nearest your players. Left blank, Cloudflare places it near you, which may be the wrong continent. It can’t be changed later without recreating
  • Specify jurisdiction: leave alone — it’s for EU data residency and overrides the location hint
the Create D1 database dialog with the name and location hint

4b. Create the tables

Open the database → Console tab → paste your schema → Execute.

Write every statement as CREATE TABLE IF NOT EXISTS so it's safe to run twice. Four tables carried me: users, identities (one row per way of signing in), purchases, entitlements.

Two design rules worth copying:

  • Key everything on your own user ID, not the auth provider’s. Store the provider’s ID in identities. That's what lets one person use email today and Google tomorrow, and what saves you if you ever change provider
  • Give purchases a status (paid / refunded / disputed) and let entitlements be revoked with a reason, never deleted. Without this, someone can buy, refund, and keep what they bought forever

Verify in the Tables tab.

4c. Connect it to your site

Workers & Pages → your project → Settings → Bindings → Add → D1 database:

  • Variable name: DB (this is the name your code looks for)
  • D1 database: your database
  • Add it, then repeat for Preview if you use preview deployments
the D1 binding panel with variable name DB

Then redeploy. See the trap in troubleshooting — this one caught me three times.

Step 5 — Connect authentication to your code

5a. The publishable key goes in your site

One line in your front-end config:

JavaScript

publishableKey: 'pk_live_…'

It ships in your page source. That’s fine — it identifies your Clerk instance and grants nothing on its own.

5b. The secrets go in the hosting dashboard

Workers & Pages → your project → Settings → Variables and secrets → Add:

  • Name: CLERK_PUBLISHABLE_KEY | Value: pk_live_… | Type: Text
  • Name: CLERK_SECRET_KEY | Value: sk_live_… | Type: Secret
  • Name: CLERK_WEBHOOK_SECRET | Value: from 5c | Type: Secret
  • Name: STRIPE_WEBHOOK_SECRET | Value: from 5d | Type: Secret
all four variables, three of them encrypted

Choosing Secret rather than Text means the value becomes unreadable afterwards. That’s what you want.

5c. The Clerk webhook — records signups as they happen

Clerk → Configure → Webhooks → Add Endpoint:

he webhook endpoint showing the URL and the three subscribed events

5d. The Stripe webhook — the one that protects your money

First, check you’re in the right Stripe account. Your site’s publishable key pk_live_51ABC… encodes the account ID acct_1ABC…; compare it with Settings → Account details. I have two Stripe accounts and events sent from the wrong one would simply never arrive.

Then dashboard.stripe.com/webhooks, in Live mode → Add destination.

Select these five events:

Plaintext

checkout.session.completed
checkout.session.async_payment_succeeded
charge.refunded
charge.dispute.created
charge.dispute.closed
The five selected events

The first two record sales. The last three are what let you take the benefit back when someone refunds or charges back — and restore it if you win a dispute. Skip them and you’re running an honour system.

Destination type Webhook endpoint, then:

  • Endpoint URL: [https://yourproject.pages.dev/api/stripe/webhook](https://yourproject.pages.dev/api/stripe/webhook)
  • Name it something you’ll recognise in six months
the Configure destination screen

Copy the Signing secret into STRIPE_WEBHOOK_SECRET, then redeploy.

Note: signing secrets are per endpoint. Creating a second endpoint instead of editing the first gives you a new secret and silently breaks the old one.

Step 6 — Test shared sign-in

6a. Add a health check you can trust

Build one endpoint that reports each piece separately and returns booleans only — never a key, never a database error:

JSON

{ "ok": true, "database": true, "databaseQuery": true,
"schema": "002-accounts", "clerk": true,
"clerkWebhook": true, "stripeWebhook": true }

database says the binding exists; databaseQuery says it actually works; schema proves the tables are there.

the health endpoint with everything still false, before the pieces were connected

6b. The actual test

  • Open one of your sites and sign up with a throwaway email. The code should arrive within seconds — that also proves your email DNS records work
  • Try Continue with Google. It should land in the same account, because the email matches
  • Now open a different site of yours. You should already be signed in. That’s the whole point
signed in on one edition after signing in on a completely different one
  • Check your database: SELECT * FROM users should show the account and which site it came from
  • Buy something with a Stripe test card, then refund it in Stripe. The benefit should disappear on its own, and the sale should remain on record marked refunded

Troubleshooting: the five traps

1. You saved the setting and nothing changed.

Cloudflare attaches bindings and variables when a deployment is built. Saving changes nothing until a new build runs. Deployments → latest → ⋯ → Retry deployment, or push any commit. This caught me with the database binding, the Clerk keys, and both webhook secrets.

2. DNS verification stays “pending” forever.

The records are proxied. In Cloudflare they must be DNS only — grey cloud. Proxied records never verify and produce no useful error.

3. “Automatic setup could not be confirmed.”

Often a false alarm — the tool checked faster than DNS propagates.

the red failure banner that was wrong

Before re-running anything, check reality:

Bash

dig +short CNAME clerk.yourgame.com @1.1.1.1

If it returns the target, the records are fine — just click Verify Records again. Re-running the automatic setup risks duplicates. (Bonus: if dig returns Cloudflare IP addresses instead of a CNAME target, your record is proxied — that's trap 2.)

4. Your setup check passes when nothing is configured.

If your “am I signed in?” endpoint answers “signed out” before it checks configuration — which is correct behaviour, so a broken deployment never locks players out — then it will look healthy with nothing connected at all. That’s why Step 6a exists. My original instructions had this bug; a reviewer caught it.

5. You can’t find the API keys, or you found the wrong ones.

Clerk has two pages called “API keys”. Developers → API keys is a feature letting your users mint keys — leave it disabled. 
Instance → API keys is yours.

the “Enable API keys” page

Bonus trap — the download that nearly went public. Google’s console saves a client_secret_*.json to your downloads folder. Mine landed in my project folder, which is also a website's root directory. It was never committed, so nothing leaked — but one routine git add -A would have published my OAuth secret at [yourgame.com/client_secret](https://yourgame.com/client_secret)_….json. Add client_secret*.json, *service-account*.json and .env* to your .gitignore before you need them.

What this costs

  • Clerk: Free until 50,000 monthly users
  • Cloudflare D1: Free until 5 GB, 5M row reads/day
  • Cloudflare Pages Functions: Free until 100,000 requests/day
  • Google OAuth: Free always

Free-plan trade-offs worth knowing: sessions last 7 days before re-login, there’s a small “secured by Clerk” badge, no two-factor auth, and three social logins.

Total elapsed time: about two days including the mistakes. Following these steps, closer to two hours.

The one decision that determines whether any of this stays free is Step 1. Get your sites onto one domain before you touch an auth provider — everything after that is filling in forms.


I’m a New York-based software and data engineer and a recent master’s graduate. This is just one example of the work I do.

I’m currently open to project-based work, and consulting. If you, your team, or someone in your network could use my skills, I’d love to connect.

I’m always happy to have a coffee chat and explore how I could help. Feel free to reach out.

Comments

Popular posts from this blog

Briefing Document: The State of AI - How Organizations Are Rewiring to Capture Value (McKinsey, March 2025)

Cracking the Code of Online Popularity: Lessons from Feature Selection and PCA