CrydenSync

Features

Every feature Cryden ships, organized by which Config fields turn it on and which public functions you call to use it.

Cryden is a pick-a-feature menu: every section below says which Config fields turn that feature on and which public functions you call to use it. All features live on one engine, so you can enable any subset, or all of them, inside a single cryden.Config.

How to use this guide

Build the base engine as shown in the next section, then add the Config fields listed under whichever feature you want and call cryden.New again. Every optional feature you leave out is off and returns a typed "not configured" error instead of panicking, so adding features is always additive, never a breaking change to what already works.

The base engine

Four Config fields are required, with no defaults: JWTSecret, Users, Sessions, and Audit. JWTSecret signs access tokens and pending login tokens, and must be a long, random value kept out of source control.

import (
	"context"
	"os"

	"github.com/crydensync/cryden/v2"
	"github.com/crydensync/cryden/v2/store/memory"
)

engine, err := cryden.New(cryden.Config{
	JWTSecret: os.Getenv("JWT_SECRET"),
	Users:     memory.NewUserStore(),
	Sessions:  memory.NewSessionStore(),
	Audit:     memory.NewAuditStore(),
})

ctx := context.Background()
user, err := cryden.SignUp(ctx, engine, "proguy@example.com", "Pass@2026", "1.2.3.4")
tokens, err := cryden.Login(ctx, engine, "proguy@example.com", "Pass@2026", "1.2.3.4", "Mozilla/5.0")
userID, err := cryden.VerifyToken(engine, tokens.AccessToken)

store/memory implements every store interface in-process and is for trying things out and for tests. For production use store/postgres, which needs migrations 0001 through 0007 from store/postgres/migrations run in order and Postgres 13 or newer, or store/sqlite, which needs a driver you register plus the Migrate and CheckPragmas helpers run against an opened database with the foreign_keys, busy_timeout, and journal_mode(WAL) DSN pragmas set. Constructor names match across backends: postgres.NewUserStore(db), sqlite.NewUserStore(db), and memory.NewUserStore() all satisfy Config.Users, and the same naming holds for Sessions, Audit, and every feature store below. callerIP and userAgent are yours to supply on every call that takes them, and are used for rate limiting, audit metadata, and device labels.

Password policy and breach checking

SignUp and ChangePassword validate against Config.PasswordPolicy. The zero value applies security.DefaultPasswordPolicy, a length floor of 8 and a ceiling of 72 with no forced character classes; setting any field makes it a real, custom policy used as-is. A rejected password returns ErrPasswordPolicyViolation carrying every broken rule at once, as stable codes such as min_length and require_uppercase.

Config.BreachedPasswordChecker is an interface with zero shipped implementations. It is consulted after local policy and ownership checks, an outage fails open, and only a confirmed breach rejects the password with ErrPasswordBreached.

Password hashing

The default hasher is bcrypt at cost 10 from Config.BcryptCost. Config.Hasher accepts a security.Hasher such as security.NewArgon2idHasher(security.DefaultArgon2idParams), which implements RFC 9106 parameters of 64 MiB, t equals 3, and p equals 4, with PHC-format output. The engine wraps the chosen hasher in a MultiHasher that identifies the algorithm from each stored hash's own prefix, so switching never locks out existing users and needs no migration. A successful login that finds an out-of-date hash rewrites it with the current hasher and records a password_hash_upgraded audit event.

Rate limiting and lockout

The default limiter is an in-process fixed-window counter of Config.RateLimitAttempts attempts per Config.RateLimitWindow, correct for one process. Config.RateLimiter accepts any security.RateLimiter, and the shipped security.NewRedisRateLimiter(client, attempts, window) runs an atomic INCR/PEXPIRE script so every engine instance counts against one shared window; it supports redis.Client, ClusterClient, and Ring, namespaces keys under a configurable prefix, and fails closed when Redis is unreachable.

Account lockout is persistent and database-backed from Config.LockoutThreshold and Config.LockoutDuration, defaulting to 5 failed attempts per 15 minutes, so it survives restarts and holds across multiple instances.

Email verification and change

RequestEmailChange and ConfirmEmailChange implement a two-step flow where the new address takes effect only after a token from the store is confirmed. Config.Verifications and Config.EmailSender are both required; delivery hands over only a raw token rather than a URL, and tokens are single-use, expiring, hashed at rest, and distinguished by purpose so an email-change token can never be replayed as a magic link, or vice versa.

OAuth

Config.OAuth takes a store, and cryden.LoginWithOAuth is provider-agnostic, running after the host has already completed the provider's redirect. It doubles as signup, and refuses to auto-link when the provider email matches an existing password account, returning ErrOAuthEmailConflict instead; the resolution is a password login followed by LinkOAuthIdentity with a user ID from an already-verified session. OAuth logins route through the same second-factor gate as every other primary method.

TOTP

Config.TOTP and Config.EncryptionKey together turn on TOTP enrollment and completion: EnrollTOTP returns an otpauth:// URL, ConfirmTOTP is the required two-step confirm before the account demands codes, DisableTOTP requires the current password, and CompleteLoginWithTOTP finishes a paused login. Config.TOTPIssuerName defaults to Cryden. Secrets are encrypted at rest with AES-GCM derived from EncryptionKey rather than hashed, because validating a code requires recovering the secret.

Passkeys

Config.WebAuthn with its WebAuthnRPID, WebAuthnRPDisplayName, and WebAuthnRPOrigins fields turns on the ceremony pair BeginRegisterPasskey/FinishRegisterPasskey plus ListPasskeys and DeletePasskey. Passkeys are cryptographically bound to the RPID, ceremony state is encrypted with the same EncryptionKey, and credentials are stored as opaque blobs with a denormalized credential ID for indexed matching. Passkeys are a second factor: BeginWebAuthnLogin and CompleteLoginWithWebAuthn finish a paused login the same way a TOTP code does.

Recovery codes

Config.RecoveryCodes turns on GenerateRecoveryCodes and CompleteLoginWithRecoveryCode. Codes are single-use, stored only as hashes, shown exactly once, and atomically replaced by a new batch. They are only offered in the second-factor Methods list alongside a real factor, so they can never become a standalone backdoor.

Config.Verifications and Config.MagicLinkSender together turn on RequestMagicLink and CompleteMagicLink. Links log in existing accounts only, return nil for nonexistent emails to avoid enumeration, are valid for fifteen minutes, are single-use, and completion routes through the same second-factor gate as a password login. MagicLinkSender is a separate interface from EmailSender, so adding it never breaks an existing email implementation.

Account management and sessions

ChangePassword requires the current password and revokes all other sessions. DeleteAccount requires the current password as re-confirmation. Logout, LogoutAll, and RevokeSession verify ownership before acting. Refresh tokens rotate on every use inside session families, and presenting an already-rotated token revokes the entire family and records a token_reuse_detected audit event.

ListSessions, ListPublicSessions, ListNamedSessions, GetUser, and the store's paginated listing and counts build admin tooling without bypassing the store layer. Config.Geolocator turns on the location half of named-session labels; the device half is a built-in user-agent parser, and errors fail open to a device-only label.

Anomaly and credential-stuffing detection

Config.Anomalies turns on login anomaly detection, with thresholds from Config.AnomalyThresholds, and Config.CredentialStuffingThresholds tunes the stuffing pass that shares the same login-attempt history. Six anomaly signals ship: new_ip, new_device, user_failure_velocity, ip_failure_velocity, token_reuse, and concurrent_sessions. Stuffing emits account_spray or unknown_account_spray with a cooldown. Both are report-only, record an audit event when they fire, never block or delay a login, and degrade to no evidence on storage failure. Leaving either thresholds struct entirely zero-valued applies the defaults.

Custom claims and API keys

Config.AccessTokenClaims attaches host claims, such as role or tenant ID, to every access token on login and refresh. The seven registered RFC 7519 claim names are refused, provider errors fail the token, and VerifyTokenWithClaims reads the claims back.

Config.APIKeys, with an optional Config.APIKeyPrefix, turns on GenerateAPIKey, AuthenticateAPIKey, ListAPIKeys, and RevokeAPIKey. Raw keys are prefixed, shown once, and stored only as SHA-256 hashes; scopes are opaque host-defined strings with exact-match HasScope; failures are uniformly ErrInvalidAPIKey; and keys sit deliberately outside the session and second-factor systems.

Webhooks and audit

Config.Webhooks accepts a notify.WebhookSender, and Config.WebhookEvents defaults to cryden.DefaultWebhookEvents(), the actionable, low-volume subset that excludes login_success, login_failed, and token_rotated. Delivery is synchronous on the request path right after the audit write, an error is logged and never fails the reported operation, and events carry stable IDs for idempotent receivers.

Admin reports

cryden.WeeklyDigest and cryden.DigestSince build a plain-text digest over a default seven-day window with exact counts, attention-worthy events newest first up to ten per type, and unknown event types under an "other events" section.

cryden.DiagnoseLoginIssue answers why a user cannot log in, from lock state, failed-attempt count, current session count, and the newest failure events.

cryden.ConfigTuningReport and cryden.TuningReportSince read a 30-day window against the engine's own tuning settings and suggest changes, never applying any.

The widget package offers widget.Ask for a host's own end users: Provider and Store are the ai package's interfaces, and every parsed query is force-scoped to the owner user ID before validation, so prompt injection cannot cross the identity boundary.

See Security for the reasoning behind each of these, and Code Examples for a runnable snippet of every feature on this page.