Colocating Auth Responsibilities

How moving authentication into Beyond Cloud simplified the auth boundary across the 33-ish sites and apps I keep building.

How moving authentication into Beyond Cloud simplified the auth boundary across the 33-ish sites and apps I keep building.

I have a habit of making new sites. Some are products, some are internal tools, some are tiny experiments that become real enough to need users. At the moment there are about 33 Cloudflare Pages projects in my account. Not all of them need auth, but enough do that copying provider setup into every app has stopped feeling like "just ship it" and started feeling like infrastructure debt.

The pattern I want is simple: one place owns authentication, each app owns its authorization. Beyond Cloud should answer "who is this user?" DJ Writer, Clockzen, TKN Scope, or whatever comes next should answer "what can this user do here?"

That is what we just made concrete. Bey, the auth surface inside Beyond Cloud, now behaves much more like the shared auth service I wanted instead of a helper copied into a single app.

Why Colocate Auth?

Authentication has a lot of repeated machinery: OAuth provider secrets, callback URLs, state validation, token exchange, account linking, refresh tokens, sessions, cookies, and hosted login UI. Those responsibilities do not become more interesting when repeated across a dozen projects. They become easier to misconfigure.

Authorization is different. It is usually local and product-shaped. A writing app needs workspace membership. An analytics product needs site access. A finance app needs account boundaries. Those checks should live close to the domain model, at least until there is a shared policy system worth extracting.

So the boundary became:

  • Beyond Cloud / Bey owns authn: provider credentials, OAuth redirects, user identity, sessions, and hosted login.
  • Each app owns authz: local membership, roles, resource access, and product-specific rules.
Colocated Auth Boundary
flowchart LR
  Sites["33 sites and apps"] --> Bey["Beyond Cloud Auth"]
  Bey --> Google["Google OAuth"]
  Google --> Bey
  Bey --> Session["Application session"]
  Session --> AppPolicy["Local app authorization"]

  classDef platform fill:#eef6f2,stroke:#15803d,color:#111827
  classDef app fill:#f8fafc,stroke:#64748b,color:#111827
  class Bey,Google,Session platform
  class Sites,AppPolicy app
Provider authentication is centralized in Beyond Cloud. App authorization stays local.

The Practical Trigger

The thing that forced the boundary into focus was a Google OAuth redirect mismatch in DJ Writer. The easy fix would have been to add another Google callback URI or copy Google credentials into the app. That would have solved one login screen and made the next site worse.

wrong-boundary.txt
1 DJ Writer owns Google OAuth:
2
3 GOOGLE_CLIENT_ID=...
4 GOOGLE_CLIENT_SECRET=...
5 redirect_uri=https://api-writer.frodojo.com/api/auth/google/callback

That shape is fine if DJ Writer is the only application I will ever run. It is the wrong shape for a personal cloud with dozens of services. The provider boundary belongs to the platform.

One Provider Callback

The better fix was to keep Google pointed at the Beyond Cloud callback it already knew: https://api.usebey.com/api/auth/callback/google.

App login still starts from the consuming app, but the provider callback lands at Bey. Bey resolves the OAuth state to an application, creates the app-user session, and returns the browser to the app's callback.

appoauth.go
1 func (h *AppOAuthHandler) getProviderCallbackURL(
2 r *http.Request,
3 provider string,
4 ) string {
5 baseURL := os.Getenv("APP_AUTH_CALLBACK_BASE_URL")
6 if baseURL == "" {
7 baseURL = os.Getenv("PUBLIC_API_URL")
8 }
9 if baseURL == "" {
10 baseURL = os.Getenv("BASE_URL")
11 }
12 return strings.TrimSuffix(baseURL, "/") + "/api/auth/callback/" + provider
13 }

The shared callback route then checks whether the incoming OAuth state belongs to hosted app auth. If it does, Bey handles it as an app login. If not, the request falls back to Beyond Cloud's own platform login handler.

server.go
1 mux.HandleFunc("/api/auth/callback/", func(w http.ResponseWriter, r *http.Request) {
2 provider := strings.TrimPrefix(r.URL.Path, "/api/auth/callback/")
3 state := r.URL.Query().Get("state")
4
5 if appOAuthHandler != nil && appOAuthHandler.HasPendingOAuthState(r, state) {
6 appOAuthHandler.HandleProviderCallback(w, r, provider)
7 return
8 }
9
10 oauthHandler.HandleGoogleCallback(w, r)
11 })

The nice part is that this gives the platform one canonical provider registration while still letting every app have its own return URL and local session exchange.

What DJ Writer Keeps

DJ Writer no longer needs to know how to talk to Google. It does need to know whether a Bey-authenticated user is active locally, whether that user belongs to a workspace, and whether a document operation is allowed.

That local policy is not a failure to centralize. It is the point of the boundary. Authentication is shared because it is mostly the same across apps. Authorization stays near the app because it is where the product meaning lives.

live-check.txt
1 https://api-writer.frodojo.com/api/auth/bey/google
2
3 redirect_uri=https://api.usebey.com/api/auth/callback/google

Why This Scales

The payoff is not just that DJ Writer login works. The payoff is that the next app gets a smaller auth surface. It needs a publishable application key, a return URL, and app-specific authorization checks. It does not need a Google Console checklist, provider secrets, callback plumbing, or a bespoke OAuth implementation.

Across 33 sites and apps, that is the difference between an auth system and a pile of one-off auth implementations. Centralize the repeated identity work. Keep the domain decisions local. Promote authorization into the platform only when enough apps have proven they share the same policy shape.

That is the version of "Clerk-like" that actually matters for my setup: not copying Clerk's UI, but colocating the responsibilities so every new app starts with a clean auth boundary.