Build your application
HTTP integration
Mount auth beside your existing Effect routes. The HTTP boundary decodes the shared contract, supplies request credentials, and delivers cookies. Application handlers call the same auth service directly.
For browser queries and mutations, continue with the Effect Atom client.
- Your code: Your appCalls your own routes
- Yielded Auth: Auth client
Client.make( AuthApi, …) - Your code: Your routes
http.Reads credentials, sets cookiesmiddleware - Yielded Auth: Auth routes
Http.Decodes the contract, sets cookieslayer( AppAuth, …) - Yielded Auth: Auth service
AppAuth - Your code: Your LayersAccounts, policy, storage
- Your app to Your routes
- Auth client to Auth routes: Contract actions
- Your routes to Auth service: requireSession()
- Auth routes to Auth service
- Auth service to Your Layers
- Browser: Your app, Auth client
- Server: Your routes, Auth routes, Auth service, Your Layers
Define the routes
Section titled “Define the routes”Keep the contract safe to import in both the browser and server:
import { Schema } from "effect";import { AuthContract } from "@yielded/auth";
export const AuthApi = AuthContract.make("app/Auth", { claims: Schema.Struct({ displayName: Schema.String }), actions: (sessions) => ({ signIn: AuthContract.passwordSignIn(sessions) }),});The contract includes getSession, requireSession, signOut, and renewSession.
It exposes only the additional actions you select. Installing a strategy does
not publish all its methods.
The HTTP reference lists default routes and how to change the shared base path.
Configure the server
Section titled “Configure the server”Bind the contract to your methods and session configuration:
import { Auth, Http, Password, Sessions } from "@yielded/auth";
import { AuthApi } from "@app/domain/auth-contract";
export const AppAuth = Auth.make(AuthApi, { sessions: Sessions.stateful(), strategies: { password: Password.make() }, defaultStrategy: "password",});
export const AuthRoutes = Http.layer(AppAuth, { origin: "https://app.example.com" });Http.layer mounts the shared actions and configured OAuth callbacks, and
supplies AppAuth.layer. Provide your stores and account authority to the result.
The adapter guide shows the
AuthDependencies composition used below.
For an existing raw HttpRouter, merge the auth route Layer with your application
routes:
import { Layer } from "effect";
import { ApplicationRoutes } from "./application-routes";import { AuthRoutes } from "./auth";import { AuthDependencies } from "./auth-live";
export const Routes = Layer.mergeAll(AuthRoutes, ApplicationRoutes).pipe( Layer.provide(AuthDependencies),);Proof request admission
Section titled “Proof request admission”Email code/link and password-reset requests have built-in rate limiting. Auth shares one limiter across these methods; HTTP uses the current socket peer, so normal server setup needs no additional service or middleware.
The default allows a burst of twenty requests per network and replenishes one
request every three minutes. It checks every request before account lookup,
including retries and unknown addresses. Change the policy with
Proofs.HostIngressLimiter.layer({ limit: 40 }), or provide your own service.
The default store is local to the Auth runtime. Multiple server instances can use
a shared Effect RateLimiterStore. Behind a proxy, provide the trusted client
identity per request; forwarded headers are not trusted automatically. See the
rate-limit reference for those overrides.
Application routes
Section titled “Application routes”For application routes that call auth, build the middleware with Http.make:
import { Http } from "@yielded/auth";
import { AppAuth } from "./auth";
export const http = Http.make(AppAuth, { origin: "https://app.example.com" });Wrap the application routes before merging them with the generated auth routes:
import { Layer } from "effect";
import { ApplicationRoutes } from "./application-routes";import { AppAuth, AuthRoutes } from "./auth";import { http } from "./auth-http";import { AuthDependencies } from "./auth-live";
export const Routes = Layer.mergeAll( AuthRoutes, ApplicationRoutes.pipe(http.middleware, Layer.provide(AppAuth.layer)),).pipe(Layer.provide(AuthDependencies));The middleware reads request credentials and delivers cookies on the response. Handlers can call auth directly, including from JSON, form, and multipart routes.
Join an existing HttpApi
Section titled “Join an existing HttpApi”Add the native auth group beside your application groups in the shared API:
import { AuthContract } from "@yielded/auth";import { HttpApi } from "effect/http-api";
import { AuthApi } from "./auth-contract";import { Projects } from "./projects-contract";
export const Api = HttpApi.make("app").add(Projects, AuthContract.httpGroup(AuthApi));import { Layer } from "effect";import { HttpApiBuilder } from "effect/http-api";
import { Api } from "@app/domain/api";import { http } from "./auth-http";import { AuthLive } from "./auth-live";import { ProjectHandlers } from "./projects-handlers";
export const Routes = HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe( Layer.provide(http.handlers(Api)), Layer.provide(ProjectHandlers), http.middleware, Layer.provide(AuthLive),);Auth appears as the auth group in the API and its OpenAPI document. Set route
paths in the shared contract’s basePath; see the HTTP reference
for path and group-name options.
Protect an application handler
Section titled “Protect an application handler”Inside a route covered by http.middleware, require a session before doing the work:
import { Effect } from "effect";
import { AppAuth } from "./auth";
export const currentMember = Effect.fn("app.currentMember")(function* () { const auth = yield* AppAuth; const session = yield* auth.requireSession();
return session.claims;});requireSession() fails with AuthenticationRequired when there is no valid
session. Use getSession() for an optional session. Both read the current request
context; neither makes an HTTP call.
Protect an HttpApi group
Section titled “Protect an HttpApi group”Declare the session requirement in your shared API:
import { SessionContract } from "@yielded/auth";import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
import { AuthApi } from "./auth-contract";
export const SessionHttp = SessionContract.makeSessionHttpContract( "app/Auth", AuthApi.sessions.Session, { cookieName: "__Host-app-session" },);
export const ProfileApi = HttpApi.make("profile").add( HttpApiGroup.make("profile") .add(HttpApiEndpoint.get("current", "/me", { success: AuthApi.claims })) .middleware(SessionHttp.RequireSession),);On the server, supply the middleware and read CurrentSession in the handler:
import { Http } from "@yielded/auth";import { Effect, Layer } from "effect";import { HttpApiBuilder } from "effect/http-api";
import { ProfileApi, SessionHttp } from "@app/domain/profile-api";import { AppAuth } from "./auth";import { AuthLive } from "./auth-live";
const http = Http.make(AppAuth, { origin: "https://app.example.com", cookie: { name: SessionHttp.cookieName },});
const ProfileHandlers = HttpApiBuilder.group(ProfileApi, "profile", (handlers) => handlers.handle("current", () => Effect.map(SessionHttp.CurrentSession, (session) => session.claims), ),);
const ProfileRoutes = HttpApiBuilder.layer(ProfileApi).pipe( Layer.provide(ProfileHandlers), Layer.provide(http.securityLayer(SessionHttp)), http.middleware,);
export const Routes = Layer.mergeAll(http.routes(), ProfileRoutes).pipe(Layer.provide(AuthLive));Both route sets use the same cookie configuration. The session middleware returns 401 for absent or invalid sessions and 503 when verification is unavailable. Your handler still decides whether the authenticated account may perform an application action.
Cookies and deployment
Section titled “Cookies and deployment”For HTTPS, set your trusted application origin. The defaults use Secure,
HttpOnly, and SameSite=Lax cookies:
export const http = Http.make(AppAuth, { origin: "https://app.example.com",});For local development over plain HTTP:
export const http = Http.make(AppAuth, { origin: "http://localhost:3000", cookie: { secure: false },});The auth client sends the required CSRF header automatically. Keep custom cookie names and CSRF settings aligned with the server; cookies and request policy lists the options.
Expose another method
Section titled “Expose another method”To add passkeys alongside passwords, declare the begin and complete actions in the shared contract:
import { AuthContract, PasskeyContract } from "@yielded/auth";import { Schema } from "effect";
export const AuthApi = AuthContract.make("app/Auth", { claims: Schema.Struct({ displayName: Schema.String }), actions: (sessions) => { const passkey = PasskeyContract.make("app/Auth/passkey", sessions);
return { signIn: AuthContract.passwordSignIn(sessions), beginPasskey: AuthContract.fromOperation(passkey.operations.Begin, { strategy: "passkey", method: "signIn", }), completePasskey: AuthContract.fromOperation(passkey.operations.Complete, { strategy: "passkey", method: "completeSignIn", requestFields: { bindingCredential: "request-binding" }, subject: { fromSuccess: (result) => result._tag === "Authenticated" ? result.session.subjectId : undefined, }, }), }; },});Bind the new actions to a passkey strategy on the server:
import { Auth, Passkey, Password, Sessions } from "@yielded/auth";
import { AuthApi } from "@app/domain/auth-contract";
export const AppAuth = Auth.make(AuthApi, { sessions: Sessions.stateful(), strategies: { password: Password.make(), passkey: Passkey.make() }, defaultStrategy: "password",});The client now has beginPasskey and completePasskey actions. requestFields
supplies the private binding from a cookie; subject publishes the authenticated
account after completion. The passkey guide
shows verifier and storage Layers, and the client guide
shows the browser ceremony.
For custom actions, see action schemas and credential mapping.