Auth / foldkit-auth

On Workers with D1

examples/cloudflare runs it end to end on Miniflare: Yielded's routes (/auth/…) and /me, which answers through AuthServer.resolver, in the same Worker as serveFetch (src/authRoutes.ts); the account table and Yielded's own as a D1 migration (migrations/0005_auth.sql); sign-up provisioning the account row; and a test that signs up, signs in and reads the session back (test/auth.test.ts). Three things it shows that the sections above do not:

  • Persistence on D1 is @yielded/auth-persistence-drizzle/D1. The AuthPersistence of @yielded/auth-persistence commits a sign-in or a sign-up in an interactive transaction, which @effect/sql-d1 does not run ("transactions are not supported in D1"). The Drizzle package's D1 module commits through D1's atomic batches instead; give it D1Client.layer({ db: env.DB }) and its tables through Persistence.managed({ subjects, prefix }). Its Persistence has the Provisioning and Config of Signing up.
  • The migration is the application's. Yielded checks the columns and unique keys it reads when it first runs, and creates nothing. Write each managed table as Drizzle declares it; the example's test derives the statements from storage.schema and compares.
  • @yielded/auth-platform-workerd needs nodejs_compat and a Wasm module. Its layer gives Yielded its crypto and password hashing on Workers; the hashing imports node:crypto and an Argon2 .wasm file, and a bundle that is not Wrangler's keeps the .wasm beside the script as a compiled module. Node cannot load it, so keep the auth definition (tables included) in a module that does not import it, for tests to read.

Beyond those, a few settings a server on one machine never needs:

  • Build auth once per isolate. Make AppAuth's layer and its persistence over the D1 binding once, in a ManagedRuntime, and provide its context to each request. A D1 binding is not I/O a request owns, so sharing it is safe. Built per request, Yielded runs its storage's first-use setup again with each build, and two requests carrying one cookie at once (a refresh beside a page's read) can fail session verification, which serveFetch answers as a 500.
  • Allow the clocks to differ. Yielded's AuthenticationClock is strict by default (futureToleranceMillis: 0), and a session's issue time comes from the Worker while D1 keeps its own clock. A Worker's Date.now() moves only between I/O, so a session can read as a moment in the future, and a correct password is refused as PasswordRejected. Give the clock some room:
    ts
    Layer.provideMerge(Operations.AuthenticationClock.layer({ futureToleranceMillis: 1000 }))
  • Expect the sign-in limit to be per isolate. Yielded counts password attempts, successful ones included, per email and per account: by default 10, then one more every 6 seconds. It counts in memory unless the layer is given an Effect RateLimiterStore (effect/persistence/RateLimiter), so each isolate keeps its own count, which is lost when the isolate is evicted. Give the layer a shared store for a limit that holds across isolates. A test that signs one account in more than ten times in quick succession is refused as PasswordRejected, so sign each test account in once.