Skip to content

Build your application

How sign-in works

Follow one password sign-in from the browser to the session cookie. Yielded Auth runs the security-sensitive steps; your code supplies the accounts and storage they act on.

  1. Browser

    Call the contract

    yield* client.auth.signIn({ email, password })

    The client sends the contract's input to /auth/signIn. The password travels in the request body, never in the URL.

  2. Yielded Auth

    Decode the request

    Http.layer(AppAuth, { origin })

    The route decodes the input against the same contract, wraps the password in Redacted, and reads any session cookie into the request context.

  3. Yielded Auth

    Verify the password

    Password.make()

    The password method finds the credential through your password storage and checks it with the hashing Layer you supply.

  4. Your code

    Resolve the account

    SessionClaims.resolve({ subjectId, credential })

    Your account code returns the session claims, such as displayName, or rejects a disabled account.

  5. Your code

    Commit the session

    Your session storage writes the new session in one transaction, whether it uses managed tables, your own schema, or a custom service.

  6. Yielded Auth

    Set the cookie

    Only after the commit does the session cookie go on the response. Credentials never appear in the result.

  7. Browser

    Read the result

    { _tag: "Authenticated", session }

    The client decodes a session with your typed claims. A wrong password arrives as a typed PasswordRejected failure instead.

SessionClaims is the Effect service your application implements to turn a verified identity into session data. For password sign-in:

apps/server/auth-claims.ts
import { Password } from "@yielded/auth";
import { Effect, Layer } from "effect";
import { AppAuth } from "./auth";
import { Customers } from "./customers";
export const ClaimsLive = Layer.effect(
AppAuth.strategies.password.SessionClaims,
Effect.gen(function* () {
const customers = yield* Customers;
return {
resolve: Effect.fn("Auth.passwordClaims")(
function* ({ subjectId }) {
const customer = yield* customers.findById(subjectId);
if (customer === null || !customer.enabled)
return yield* Password.PasswordUnavailable.make({});
return { displayName: customer.displayName };
},
Effect.mapError(() => Password.PasswordUnavailable.make({})),
),
};
}),
);

Customers is your application’s account service. Here it returns a customer or null; database access stays in that service.

Each sign-in strategy exposes SessionClaims. Use subjectId for account lookups; credential contains method-specific details such as the password’s email identifier. OAuth also supplies the verified provider identity.

Provide the claims Layer alongside hashing and storage:

apps/server/auth-live.ts
import { Layer } from "effect";
import { AppAuth } from "./auth";
import { ClaimsLive } from "./auth-claims";
import { AuthDependencies } from "./auth-dependencies";
import { PasswordPersistenceLive } from "./auth-persistence";
import { CustomersLive } from "./customers";
import { HashingLive } from "./hashing";
export const AuthLive = AppAuth.layer.pipe(
Layer.provide(ClaimsLive.pipe(Layer.provide(CustomersLive))),
Layer.provide(HashingLive),
Layer.provide(PasswordPersistenceLive),
Layer.provide(AuthDependencies),
);

Use the Argon2id Layer for hashing and your chosen adapter for storage. AuthDependencies supplies session authority, persistence, and keys.

Multi-step methods return a public flow ID between calls; the private binding that ties the steps to one browser travels as a cookie, like the session.

Method Calls
Password signIn → session or additional factor.
Email code or magic link beginSignIn → signIn sends the proof → verifySignIn → completeSignIn.
SMS code signIn sends the code → completeSignIn.
Passkey signIn → browser ceremony → completeSignIn.
OAuth signIn → provider redirect → completeSignIn.
TOTP First factor returns a pending proof → verifyPending.

Commands that change an account, such as registration or a password reset, take a request ID. A retry with the same ID returns the stored receipt instead of running the command again. If a commit’s outcome is unknown, look the receipt up through your storage adapter rather than issuing credentials again.