Cedros Login Server
Warning: Development Preview
This package is in early development (v0.0.x) and is not ready for production use. APIs may change without notice. Use at your own risk.
Production-ready authentication server with multi-tenancy, flexible auth methods, and comprehensive access control.
Features
Authentication Methods
- Email/Password: Registration and login with Argon2id password hashing
- Google OAuth: Sign-in via Google ID token verification
- Apple Sign In: Sign-in via Apple ID token verification
- Solana Wallet: Sign-in by signing a challenge message with Ed25519
- Instant Link: Passwordless email authentication
- WebAuthn/Passkeys: Passwordless authentication with passkeys and security keys
- TOTP MFA: Time-based one-time password with recovery codes
Multi-Tenancy
- Organizations: Create and manage workspaces
- Memberships: Users belong to multiple orgs with roles
- Invites: Email invitations with configurable expiry
- Org Switching: Switch active organization context
Access Control
- Built-in Roles: Owner, Admin, Member, Viewer with preset permissions
- Custom Roles: Define org-specific roles with granular permissions
- ABAC Policies: Attribute-based access control for fine-grained rules
- Authorization API: Check permissions via POST /authorize
Security
- JWT Tokens: Short-lived access tokens with refresh rotation
- Refresh Reuse Alerts: Reuse of rotated refresh tokens revokes all sessions and triggers security notifications
- Cookie Support: HTTP-only secure cookies for token storage
- Token Responses: When cookie auth is enabled, auth endpoints omit token fields from JSON responses
- Rate Limiting: Configurable sliding window rate limiter
- Login Lockout: Progressive lockout after failed attempts
- New Device Alerts: Security emails for unrecognized devices
- Audit Logging: Track all auth events with IP, user agent, and session ID
- TOTP Replay Protection: Each code can only be used once (S-14)
- Encryption at Rest: TOTP secrets encrypted with AES-256-GCM (S-22). Uses
TOTP_ENCRYPTION_SECRETor falls back toJWT_SECRET. - Step-Up Required for MFA Enrollment: MFA setup/enabling requires recent strong authentication
- Admin-Enforced MFA: Admins can require email/password users to set up TOTP via
security_require_mfasetting. Prompted as a non-skippable post-login action. Does not affect OAuth, passkey, or wallet users. - Production Validation: Enforces COOKIE_SECURE and CORS_ORIGINS in production
Compliance & Gating
- KYC (Stripe Identity): Identity verification via Stripe Identity sessions with webhook-driven status updates
- Accredited Investor Verification: Document-based accreditation with admin review workflow
- Sanctions Screening: OFAC/SDN sanctions list screening with automatic refresh
- Token Gating: Require token holdings for access (configured via admin system settings)
- Compliance API: Server-to-server endpoint for aggregated compliance status (KYC + accreditation + sanctions)
Referrals & Rewards
- Referral Codes: Auto-generated or custom referral codes per user
- Reward Tracking: Configurable rewards for referrals (credits or crypto payouts)
- Payout Management: Admin batch processing and individual payout control
- Payout Wallets: Users set their preferred payout wallet address
Communications
- Outbox Pattern: Reliable async email delivery
- Email Templates: Verification, password reset, instant link, security alerts
- Retry with Backoff: Exponential backoff for failed deliveries
Storage
- PostgreSQL: Production-ready with sqlx migrations
- In-Memory: Development mode with no external dependencies
Quick Start
Prerequisites
- Rust 1.70+ (
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh) - PostgreSQL 14+ (optional, uses in-memory storage by default)
Run Locally
# Copy environment file
# Edit .env and set JWT_SECRET (required)
# Generate with: openssl rand -base64 32
#
# Optional:
# - FRONTEND_URL for email links
# - SSO_CALLBACK_URL to override the SSO redirect (useful behind proxies)
# - AUTH_BASE_PATH to override the auth router base path (default: /auth)
# Run with in-memory storage (development)
# Or run with PostgreSQL
The server starts at http://localhost:8080.
First-Run Admin Setup
On a fresh install (no users in the database), you need to create the first admin account. There are two ways:
Option A: UI Setup Wizard (recommended)
If you're using @cedros/login-react, the <CedrosAdminDashboard> component automatically detects that no admin exists and shows a WordPress-style setup wizard. Just open the admin page in your browser — it will prompt for email, password, name, and organization name.
Option B: API call
# 1. Check if setup is needed
# → {"needsSetup":true,"hasAdmin":false,"serverVersion":"0.0.38"}
# 2. Create the first admin (only works when no admin exists)
# → {"success":true,"userId":"...","message":"Admin account created successfully. You can now log in."}
Important: The setup endpoints (
/setup/status,/setup/admin) are served at the root path, not underAUTH_BASE_PATH. They require no authentication.POST /setup/adminreturns 403 if any admin already exists — it's a one-time operation.
After creating the admin, log in with those credentials and access the admin dashboard.
Using Docker
# Start PostgreSQL only
# Start everything (requires building the image)
API Endpoints
All endpoints are served under AUTH_BASE_PATH (default: /auth). Paths below are relative to that base path, except for setup and health endpoints which are at the root.
Setup (Root Path — No Auth)
These endpoints are served at the root path, not under AUTH_BASE_PATH. They require no authentication.
| Method | Path | Description |
|---|---|---|
GET |
/setup/status |
Check if first-run setup is needed |
POST |
/setup/admin |
Create the first admin user (one-time only, 403 if admin exists) |
GET |
/health |
Health check |
GET |
/ready |
Readiness check (503 when critical dependencies are degraded) |
Authentication
| Method | Path | Description |
|---|---|---|
POST |
/register |
Email/password registration |
POST |
/login |
Email/password login |
POST |
/logout |
Logout and revoke session |
POST |
/refresh |
Refresh access token |
GET |
/user |
Get current user |
POST |
/google |
Google ID token authentication |
POST |
/apple |
Apple ID token authentication |
POST |
/solana/challenge |
Get Solana sign-in challenge |
POST |
/solana |
Verify Solana signature |
WebAuthn / Passkeys
| Method | Path | Description |
|---|---|---|
POST |
/webauthn/register/options |
Get registration options for new passkey |
POST |
/webauthn/register/verify |
Complete passkey registration |
POST |
/webauthn/auth/options |
Get authentication options (with email) |
POST |
/webauthn/auth/options/discoverable |
Get options for username-less login |
POST |
/webauthn/auth/verify |
Complete passkey authentication |
Email Verification & Password Reset
| Method | Path | Description |
|---|---|---|
POST |
/send-verification |
Send email verification link |
POST |
/verify-email |
Verify email with token |
POST |
/forgot-password |
Request password reset email |
POST |
/reset-password |
Reset password with token |
Instant Link
| Method | Path | Description |
|---|---|---|
POST |
/instant-link |
Send instant link email |
POST |
/instant-link/verify |
Verify instant link and login |
MFA (TOTP)
Scope: TOTP 2FA applies to email/password login only (
POST /login→POST /login/mfa). OAuth (Google, Apple), passkey, and Solana wallet logins are not gated by TOTP — those methods rely on their provider's own verification (e.g., Google's MFA, device-bound passkeys).
| Method | Path | Description |
|---|---|---|
POST |
/mfa/setup |
Generate TOTP secret and QR code |
POST |
/mfa/enable |
Enable MFA with verification code |
POST |
/mfa/disable |
Disable MFA |
GET |
/mfa/status |
Get MFA status |
POST |
/mfa/verify |
Verify MFA code for authenticated step-up |
POST |
/mfa/recovery |
Use recovery code for authenticated step-up |
POST |
/mfa/recovery |
Use recovery code |
Organizations
| Method | Path | Description |
|---|---|---|
GET |
/orgs |
List user's organizations |
POST |
/orgs |
Create organization |
GET |
/orgs/:org_id |
Get organization details |
PATCH |
/orgs/:org_id |
Update organization |
DELETE |
/orgs/:org_id |
Delete organization |
POST |
/orgs/:org_id/switch |
Switch active organization |
Members
| Method | Path | Description |
|---|---|---|
GET |
/orgs/:org_id/members |
List organization members |
PATCH |
/orgs/:org_id/members/:user_id |
Update member role |
DELETE |
/orgs/:org_id/members/:user_id |
Remove member |
Custom Roles
| Method | Path | Description |
|---|---|---|
GET |
/orgs/:org_id/roles |
List custom roles |
POST |
/orgs/:org_id/roles |
Create custom role |
GET |
/orgs/:org_id/roles/:role_id |
Get custom role |
PATCH |
/orgs/:org_id/roles/:role_id |
Update custom role |
DELETE |
/orgs/:org_id/roles/:role_id |
Delete custom role |
POST |
/orgs/:org_id/roles/:role_id/default |
Set default role for new members |
ABAC Policies
| Method | Path | Description |
|---|---|---|
GET |
/orgs/:org_id/policies |
List ABAC policies |
POST |
/orgs/:org_id/policies |
Create ABAC policy |
GET |
/orgs/:org_id/policies/:policy_id |
Get ABAC policy |
PATCH |
/orgs/:org_id/policies/:policy_id |
Update ABAC policy |
DELETE |
/orgs/:org_id/policies/:policy_id |
Delete ABAC policy |
Invites
| Method | Path | Description |
|---|---|---|
GET |
/orgs/:org_id/invites |
List pending invites |
POST |
/orgs/:org_id/invites |
Create invite |
DELETE |
/orgs/:org_id/invites/:invite_id |
Cancel invite |
POST |
/orgs/:org_id/invites/:invite_id/resend |
Resend invite email |
POST |
/invites/accept |
Accept invite (public) |
Authorization
| Method | Path | Description |
|---|---|---|
POST |
/authorize |
Check if action is allowed |
POST |
/permissions |
Get user's permissions in org |
Sessions
| Method | Path | Description |
|---|---|---|
GET |
/sessions |
List active sessions |
DELETE |
/sessions |
Revoke all sessions (logout everywhere) |
Credentials
| Method | Path | Description |
|---|---|---|
GET |
/user/credentials |
List all user credentials (passwords, passkeys, OAuth) |
PATCH |
/user/credentials/:id |
Update credential (e.g., label) |
DELETE |
/user/credentials/:id |
Unlink credential |
Wallet (Server-Side Signing)
| Method | Path | Description |
|---|---|---|
POST |
/wallet/enroll |
Create wallet with Shamir shares |
GET |
/wallet/material |
Get wallet metadata (pubkey, auth method) |
GET |
/wallet/status |
Check wallet enrollment and unlock status |
POST |
/wallet/unlock |
Unlock wallet for session-based signing |
POST |
/wallet/lock |
Explicitly lock wallet |
POST |
/wallet/sign |
Sign transaction (uses cached key if unlocked) |
POST |
/wallet/rotate-user-secret |
Re-encrypt Share A with new credential |
GET |
/wallet/list |
List all wallets (default + per-API-key) |
POST |
/wallet/rotate |
Rotate wallet (replace keypair, irreversible) |
User Withdrawals
Withdraw SOL/SPL tokens from the user's embedded wallet to an external Solana address. Gated by the feature_user_withdrawals system setting (disabled by default). All endpoints use auth-sensitive rate limiting (10 req/60s per IP+path).
| Method | Path | Description |
|---|---|---|
GET |
/wallet/withdraw/balances |
Get SOL + SPL token balances from wallet |
POST |
/wallet/withdraw/sol |
Withdraw native SOL to external address |
POST |
/wallet/withdraw/spl |
Withdraw SPL token to external address |
GET |
/wallet/withdraw/history |
Get paginated withdrawal history |
Credits
| Method | Path | Description |
|---|---|---|
GET |
/credits/balance |
Get all credit balances |
GET |
/credits/history |
Get credit transaction history |
GET |
/credits/holds |
Get pending credit holds |
GET |
/credits/usage |
Get credit usage analytics |
POST |
/credits/refund-request |
Submit a refund request for an original credit transaction |
User Lookup (Server-to-Server)
These endpoints require system admin authentication (API key/JWT) and are intended for server-to-server flows (e.g. payments/webhooks) where only an external identifier is available.
| Method | Path | Description |
|---|---|---|
GET |
/users/by-wallet/:wallet_address |
Resolve user_id for a Solana wallet address |
GET |
/users/by-stripe-customer/:stripe_customer_id |
Resolve user_id for a Stripe customer |
POST |
/users/by-stripe-customer/:stripe_customer_id/link |
Link a Stripe customer to a user |
Referrals & Rewards
| Method | Path | Description |
|---|---|---|
GET |
/referral |
Get current user's referral code and stats |
POST |
/referral/regenerate |
Regenerate referral code |
POST |
/referral/set-code |
Set a custom referral code |
GET |
/referral/rewards |
Get rewards configuration and earned totals |
GET |
/referral/rewards/history |
Get paginated reward history |
POST |
/referral/payout-wallet |
Set payout wallet address |
KYC (Identity Verification)
| Method | Path | Description |
|---|---|---|
POST |
/kyc/start |
Start a Stripe Identity verification session |
GET |
/kyc/status |
Get current KYC verification status |
POST |
/webhook/kyc |
Stripe Identity webhook callback |
Accredited Investor Verification
| Method | Path | Description |
|---|---|---|
GET |
/accreditation/status |
Get current accreditation status |
POST |
/accreditation/submit |
Submit accreditation application |
POST |
/accreditation/upload |
Upload supporting document (10 MB limit) |
GET |
/accreditation/submissions |
List user's accreditation submissions |
Features Discovery
| Method | Path | Description |
|---|---|---|
GET |
/features |
Get enabled features (includes kycEnabled, accreditationEnabled, tokenGatingEnabled) |
API Keys
| Method | Path | Description |
|---|---|---|
GET |
/user/api-key |
Get API key metadata (legacy, single-key) |
POST |
/user/api-key/regenerate |
Regenerate API key (legacy, returns full key once) |
GET |
/user/api-keys |
List all API keys for current user |
POST |
/user/api-keys |
Create new API key with label |
DELETE |
/user/api-keys/:key_id |
Delete a specific API key |
POST |
/auth/validate-api-key |
Validate API key (server-to-server) |
Audit Logs
| Method | Path | Description |
|---|---|---|
GET |
/orgs/:org_id/audit |
Get org audit logs (admin only) |
GET |
/admin/audit |
Get system audit logs (system admin) |
Admin
| Method | Path | Description |
|---|---|---|
GET |
/admin/users |
List all users (system admin) |
GET |
/admin/users/stats |
Get user statistics |
GET |
/admin/users/{user_id} |
Get user details (system admin) |
PATCH |
/admin/users/{user_id} |
Update user |
DELETE |
/admin/users/{user_id} |
Delete user |
PATCH |
/admin/users/{user_id}/system-admin |
Set system admin status |
POST |
/admin/users/{user_id}/force-password-reset |
Force password reset for user |
GET |
/admin/users/{user_id}/credits |
Get user credit balances |
POST |
/admin/users/{user_id}/credits |
Adjust user credits |
GET |
/admin/users/{user_id}/deposits |
Get user deposit history |
GET |
/admin/users/{user_id}/withdrawal-history |
Get user withdrawal history |
GET |
/admin/users/{user_id}/referrals |
Get user referral details |
GET |
/admin/orgs |
List all orgs (system admin) |
GET |
/admin/orgs/{org_id} |
Get org details (system admin) |
GET |
/admin/settings |
Get all system settings grouped by category |
PATCH |
/admin/settings |
Update system settings |
POST |
/admin/settings/regenerate/{key} |
Regenerate a setting value |
GET |
/admin/dashboard-permissions |
Get admin dashboard permissions |
PUT |
/admin/dashboard-permissions |
Update admin dashboard permissions |
GET |
/admin/disposable-domains |
Get disposable email domain blocklist |
PUT |
/admin/disposable-domains |
Update disposable email domain blocklist |
Admin: KYC
| Method | Path | Description |
|---|---|---|
GET |
/admin/users/{user_id}/kyc |
Get user KYC verification details |
POST |
/admin/users/{user_id}/kyc/override |
Override user KYC status |
Admin: Accreditation
| Method | Path | Description |
|---|---|---|
GET |
/admin/accreditation/pending |
List pending accreditation submissions |
GET |
/admin/users/{user_id}/accreditation |
Get user accreditation status |
GET |
/admin/accreditation/{submission_id} |
Get accreditation submission details |
POST |
/admin/accreditation/{submission_id}/review |
Approve or reject accreditation |
POST |
/admin/users/{user_id}/accreditation/override |
Override user accreditation status |
GET |
/admin/accreditation/documents/{doc_id}/url |
Get presigned URL for uploaded document |
Admin: Sanctions
| Method | Path | Description |
|---|---|---|
GET |
/admin/sanctions/stats |
Get sanctions screening statistics |
POST |
/admin/sanctions/refresh |
Refresh sanctions list from upstream |
Admin: Compliance (Server-to-Server)
| Method | Path | Description |
|---|---|---|
GET |
/admin/users/{user_id}/compliance |
Get aggregated compliance status (KYC + accreditation + sanctions) |
Admin: Referral Payouts
| Method | Path | Description |
|---|---|---|
GET |
/admin/referral-stats |
Get referral program statistics |
GET |
/admin/referral-payouts |
Get payout summary |
POST |
/admin/referral-payouts/process |
Process batch of pending payouts |
POST |
/admin/referral-payouts/retry-failed |
Retry all failed payouts |
GET |
/admin/referral-payouts/list |
List all individual payouts |
POST |
/admin/referral-payouts/{id}/process |
Process a single payout |
POST |
/admin/referral-payouts/{id}/cancel |
Cancel a pending payout |
Admin: SSO Providers
| Method | Path | Description |
|---|---|---|
GET |
/admin/sso-providers |
List SSO providers |
POST |
/admin/sso-providers |
Create SSO provider |
GET |
/admin/sso-providers/{id} |
Get SSO provider details |
PUT |
/admin/sso-providers/{id} |
Update SSO provider |
DELETE |
/admin/sso-providers/{id} |
Delete SSO provider |
Admin: Treasury
| Method | Path | Description |
|---|---|---|
POST |
/admin/treasury/authorize |
Authorize treasury wallet |
GET |
/admin/treasury |
Get treasury configuration |
DELETE |
/admin/treasury |
Revoke treasury authorization |
System Settings
Runtime-configurable settings stored in the database. Changes take effect immediately without server restart.
Categories:
privacy- Privacy period before withdrawalwithdrawal- Withdrawal worker configuration (poll interval, batch size, timeouts)rate_limit- Rate limiting thresholds (auth, general, credit, window)
# Get all settings
# Update settings
Privacy Cash Admin (Deposits & Withdrawals)
| Method | Path | Description |
|---|---|---|
GET |
/admin/deposits |
List all deposits (system admin) |
GET |
/admin/deposits/stats |
Get deposit statistics |
GET |
/admin/withdrawals/pending |
List pending withdrawals |
POST |
/admin/withdrawals/:id/process |
Process single withdrawal |
POST |
/admin/withdrawals/process-all |
Process all ready withdrawals |
GET |
/admin/credits/stats |
Get credit spending statistics |
GET |
/admin/credits/refund-requests |
List credit refund requests |
POST |
/admin/credits/refund-requests/:id/process |
Process a credit refund request (ledger reversal) |
POST |
/admin/credits/refund-requests/:id/reject |
Reject a credit refund request |
GET |
/admin/privacy/status |
Get Privacy Cash system status |
Get System Status
Returns current Privacy Cash configuration and sidecar connection status:
Response:
Process Single Withdrawal
Process a specific withdrawal. Can force early withdrawal (before privacy period ends) with confirmation:
# Process withdrawal (must be past privacy period)
# Force early withdrawal (before privacy period)
Response:
Process All Withdrawals
Process ready withdrawals in a bounded batch (safe for large datasets). Re-run to continue processing:
Response:
Webhooks
| Method | Path | Description |
|---|---|---|
POST |
/webhook/deposit |
Handle deposit notifications (Helius/Quicknode) |
POST |
/webhook/kyc |
Handle Stripe Identity verification webhooks |
Health
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/ready |
Readiness check |
Usage Examples
Register and Login
# Register
# Login
Organization Management
# Create organization
# List organizations
# Switch active organization
Invite Team Members
# Create invite
# Accept invite (by invitee)
Check Authorization
# Check if user can perform action
# Response: {"allowed": true}
MFA Setup
Note: MFA during login is completed via POST /login/mfa using the temporary mfaToken
returned by POST /login. The /mfa/verify and /mfa/recovery endpoints are intended
for authenticated step-up checks, not initial login.
# Setup MFA (returns secret and QR code)
# Enable MFA with TOTP code
Credits Refunds
Refunds are a ledger reversal implemented as a positive adjustment linked back to the original
credit transaction (referenceType='refund', referenceId=<originalTransactionId>).
User request:
# 1) Look up the original credits transaction
# 2) Submit a refund request referencing the original transaction
Admin review + process:
# List refund requests
# Process a request (can be partial, but cannot exceed remaining refundable amount)
# Reject a request
Configuration
All configuration is via environment variables. See .env.example for the complete list.
Required
| Variable | Description |
|---|---|
JWT_SECRET |
Secret key for JWT signing (min 32 chars) |
Optional
| Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
Server bind address |
PORT |
8080 |
Server port |
DATABASE_URL |
- | PostgreSQL connection URL |
JWT_RSA_PRIVATE_KEY |
- | RSA private key (PKCS#1 PEM). Required in production for stable JWT signing across restarts/instances |
CORS_ORIGINS |
http://localhost:3000 |
Allowed origins (comma-separated) |
RATE_LIMIT_ENABLED |
true |
Enable rate limiting |
RATE_LIMIT_STORE |
memory |
Rate limit store backend (memory or redis) |
REDIS_URL |
- | Redis connection URL. Required when RATE_LIMIT_STORE=redis |
COOKIE_ENABLED |
true |
Enable cookie-based token storage |
EMAIL_ENABLED |
true |
Enable email/password auth |
EMAIL_REQUIRE_VERIFICATION |
false |
Require email verification (defaults to true in production) |
WEBAUTHN_ENABLED |
false |
Enable WebAuthn/passkey support |
WEBAUTHN_RP_ID |
- | WebAuthn relying party ID (e.g. example.com) |
WEBAUTHN_RP_NAME |
- | WebAuthn relying party name shown to users |
WEBAUTHN_RP_ORIGIN |
- | WebAuthn origin (e.g. https://login.example.com) |
WEBAUTHN_CHALLENGE_TTL |
300 |
WebAuthn challenge TTL in seconds |
WEBAUTHN_ALLOW_PLATFORM |
true |
Allow platform authenticators (built-in passkeys) |
WEBAUTHN_ALLOW_CROSS_PLATFORM |
true |
Allow cross-platform authenticators (security keys) |
WEBAUTHN_REQUIRE_UV |
true |
Require user verification (biometric/PIN) |
GOOGLE_CLIENT_ID |
- | Google OAuth client ID |
SMTP_HOST |
- | SMTP server for emails |
SMTP_USERNAME |
- | SMTP username |
SMTP_PASSWORD |
- | SMTP password |
EMAIL_FROM |
- | From address for emails |
WALLET_ENABLED |
false |
Enable server-side signing wallet |
WALLET_RECOVERY_MODE |
share_c_only |
Recovery mode: share_c_only (app-locked) or full_seed (portable) |
WALLET_UNLOCK_TTL |
900 |
Session unlock TTL in seconds (default 15 min) |
PRIVACY_CASH_ENABLED |
false |
Enable Privacy Cash deposits |
PRIVACY_PERIOD_SECS |
604800 |
Privacy period before withdrawal (default 7 days) |
WITHDRAWAL_POLL_INTERVAL_SECS |
3600 |
Auto-withdrawal poll interval (default 1 hour) |
WITHDRAWAL_BATCH_SIZE |
10 |
Max withdrawals per poll cycle |
WITHDRAWAL_PERCENTAGE |
100 |
% of ready withdrawals per cycle (1-100) |
PARTIAL_WITHDRAWAL_COUNT |
0 |
Max partial withdrawals per batch (0=disabled) |
PARTIAL_WITHDRAWAL_MIN_LAMPORTS |
500000000 |
Min balance for partial withdrawal (0.5 SOL) |
DEPOSIT_WEBHOOK_SECRET |
- | HMAC secret for Helius/Quicknode webhooks |
SSO (OIDC) Notes
- Issuer URLs must use
httpsin production. - Provider scopes must include
openidandemail. POST /auth/sso/startacceptsaccessCodeandreferralin addition toorgIdandredirectUri.- When
signup_access_code_enabledis true, embedders must forwardaccessCodeon the SSO start request so the callback can apply the normal signup gate before creating a new SSO user.
Example SSO start request:
Library Usage
Embed the auth router in your own Axum application:
use ;
use Arc;
async
Custom Callbacks
Implement the AuthCallback trait to hook into auth events:
use ;
use async_trait;
use Value;
;
Custom Email Service
Implement the EmailService trait for your email provider:
use ;
use async_trait;
Embedded Wallet (Server-Side Signing)
The server includes an optional embedded Ed25519 wallet using 2-of-3 Shamir Secret Sharing for key management. The server performs all signing operations—users never see the raw private key.
Architecture
| Share | Storage | Protection | Purpose |
|---|---|---|---|
| Share A | Server (encrypted) | User credential (password/PIN/passkey) | Decrypted JIT for signing |
| Share B | Server (plaintext) | SSS math protects it | Combined with A for signing |
| Share C | User backup | 24-word recovery phrase | Disaster recovery |
Auth Methods for Share A
- Email/password users: Reuse their login password
- OAuth users (Google/Apple): Create a 6+ digit PIN
- Passkey users: Use passkey PRF extension
Session-Based Unlock
Instead of requiring credentials for every sign operation, users unlock their wallet once per session:
POST /wallet/unlockwith credential → server caches derived key- Subsequent
POST /wallet/signrequests use cached key (no credential needed) - Key auto-expires after TTL (default 15 min) or on explicit
POST /wallet/lock
Recovery Modes
Configure via WALLET_RECOVERY_MODE:
| Mode | Recovery Phrase | Portability |
|---|---|---|
share_c_only (default) |
Share C only | Can recover within app only |
full_seed |
Full 32-byte seed | Can use wallet elsewhere |
Security Model
- Server never has the full seed at rest (only JIT during signing)
- Server compromise → encrypted Share A + plaintext Share B → cannot sign without user credential
- Device compromise → nothing (shares stored server-side)
- Keys are zeroized from memory immediately after signing
Privacy Cash (Deposits & Withdrawals)
Optional privacy-preserving deposit system using the Privacy Cash protocol. Users deposit SOL/SPL tokens to their embedded wallet, funds are held for a configurable privacy period, then automatically withdrawn to the company wallet.
Deposit Types & Recovery Modes
Deposits work in all wallet recovery modes, but private (privacy-preserving) deposits require WALLET_RECOVERY_MODE=none:
| Recovery Mode | Private Deposits | Public Deposits | Config Value |
|---|---|---|---|
| None | ✅ Available | ✅ Available | none |
| Share C Only | ❌ Blocked | ✅ Available | share_c_only |
| Full Seed | ❌ Blocked | ✅ Available | full_seed |
Why private deposits require no-recovery mode: In recovery modes where users can export their private key, they could front-run withdrawal transactions by extracting their key and signing before the Privacy Cash relayer processes the batched withdrawal.
The /deposit/config endpoint returns privateDepositsEnabled: false when recovery mode is enabled, allowing the UI to automatically route users to public deposit methods.
How It Works
- Deposit: User sends SOL/USDC/USDT to their embedded wallet address
- Privacy Period: Funds held in Privacy Cash account (default 7 days)
- Auto-Withdrawal: Background worker processes ready withdrawals to company wallet
- Credit: User's account credited with deposited amount
Timing Analysis Protection
To prevent correlation of deposits and withdrawals, the system supports two layers of timing obfuscation:
Withdrawal Percentage
Spread withdrawals over multiple cycles by only processing a percentage per batch:
# Process only 20% of ready withdrawals each hour
WITHDRAWAL_PERCENTAGE=20
WITHDRAWAL_POLL_INTERVAL_SECS=3600
With 10 ready withdrawals and 20% setting, only ~2 withdrawals per hour (randomly selected).
Partial Withdrawals
Additionally, some withdrawals can be split across multiple cycles:
# Up to 3 partial withdrawals per batch (30-70% of balance each)
PARTIAL_WITHDRAWAL_COUNT=3
PARTIAL_WITHDRAWAL_MIN_LAMPORTS=500000000 # 0.5 SOL minimum for partials
Sessions with balance ≥ 0.5 SOL may have a random portion (30-70%) withdrawn, leaving the remainder for future cycles. This adds variance to both timing AND amounts.
Example Privacy Configuration
For maximum timing obfuscation:
PRIVACY_PERIOD_SECS=604800 # 7 day minimum hold
WITHDRAWAL_POLL_INTERVAL_SECS=3600 # Check every hour
WITHDRAWAL_BATCH_SIZE=20 # Claim up to 20 at a time
WITHDRAWAL_PERCENTAGE=20 # Process ~20% per cycle
PARTIAL_WITHDRAWAL_COUNT=3 # Up to 3 partial withdrawals
PARTIAL_WITHDRAWAL_MIN_LAMPORTS=500000000 # 0.5 SOL min for partials
This configuration means:
- Deposits held for at least 7 days
- Every hour, ~20% of ready sessions processed
- Up to 3 of those may be partial (30-70%)
- Average time from "ready" to "fully withdrawn" spreads across multiple cycles
Fee Considerations
Privacy Cash charges ~0.006 SOL per withdrawal. The PARTIAL_WITHDRAWAL_MIN_LAMPORTS setting (default 0.5 SOL) ensures partial withdrawals only occur on balances large enough to avoid excessive fee overhead (~1.2% at 0.5 SOL).
Database Schema
The server uses 10 PostgreSQL tables:
| Table | Purpose |
|---|---|
users |
User accounts with profile info |
sessions |
Active login sessions |
verification_tokens |
Email verification, password reset, instant link tokens |
organizations |
Workspaces/teams |
memberships |
User-org relationships with roles |
invites |
Pending org invitations |
custom_roles |
Org-specific custom roles |
abac_policies |
Attribute-based access control rules |
audit_logs |
Security and activity audit trail |
outbox_events |
Reliable async message delivery queue |
login_attempts |
Failed login tracking for lockout |
Run Migrations
# Install sqlx-cli
# Run migrations
Role & Permission System
Built-in Roles
| Role | Permissions |
|---|---|
owner |
All permissions, can delete org |
admin |
Manage members, invites, roles, settings |
member |
Standard access, create content |
viewer |
Read-only access |
Custom Roles
Create org-specific roles with custom permissions:
ABAC Policies
Define fine-grained attribute-based rules:
Password Requirements
- Minimum 10 characters
- At least 1 uppercase letter (A-Z)
- At least 1 lowercase letter (a-z)
- At least 1 number (0-9)
- At least 1 special character (@$!%*?&#^())
Rate Limits
Default limits (configurable):
| Endpoint Type | Limit | Window |
|---|---|---|
| Auth endpoints | 10 req | 60 sec |
| General endpoints | 60 req | 60 sec |
Rate-limited responses return 429 Too Many Requests with headers:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After
By default the server binary includes both in-memory and Redis-backed rate limit
stores. Single-instance deployments can use RATE_LIMIT_STORE=memory. Multi-instance
deployments should use RATE_LIMIT_STORE=redis with REDIS_URL configured so
limits are shared across nodes.
Testing
# Run all tests
# Run with logging
RUST_LOG=debug
# Run specific test
License
MIT