mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:01:09 +02:00
2745 lines
74 KiB
Markdown
2745 lines
74 KiB
Markdown
# Central Authentication System Research Report
|
||
**Generated by:** Researcher Agent - Hive Mind System
|
||
**Date:** 2025-11-25
|
||
**Mission:** Comprehensive research on authentication technologies and best practices for central auth system
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
This report provides a comprehensive analysis of authentication technologies, security best practices, and architectural patterns for building a central authentication system for the Mana Universe monorepo ecosystem. The research covers modern auth libraries, PostgreSQL security patterns, credit/token system architectures, payment integration, and multi-app authentication strategies.
|
||
|
||
### Key Recommendations at a Glance
|
||
|
||
1. **Primary Auth Solution:** Better Auth (TypeScript-first, framework-agnostic)
|
||
2. **Database:** PostgreSQL with Row-Level Security (RLS) for multi-tenancy
|
||
3. **ORM:** Drizzle ORM (optimal for Better Auth integration)
|
||
4. **Payment Gateway:** Stripe with Connect for marketplace features
|
||
5. **JWT Strategy:** Short-lived access tokens (15min) + refresh token rotation
|
||
6. **Credit System:** Double-entry ledger pattern with idempotency keys
|
||
|
||
---
|
||
|
||
## 1. Authentication Library Comparison
|
||
|
||
### 1.1 Better Auth
|
||
|
||
**Overview:**
|
||
- Modern TypeScript-first authentication framework launched in 2024
|
||
- Framework-agnostic (React, Vue, Svelte, Astro, Next.js, Nuxt, SvelteKit, Hono)
|
||
- YC-backed (Y Combinator X25)
|
||
- Recommended by Next.js, Nuxt, Astro, and other major frameworks
|
||
|
||
**Key Features:**
|
||
- Email/password authentication
|
||
- 50+ OAuth providers (Google, GitHub, Discord, Twitter, etc.)
|
||
- Two-factor authentication (2FA) built-in
|
||
- Passkey support (WebAuthn)
|
||
- Magic link authentication
|
||
- Organization/multi-tenant management
|
||
- Multi-session support
|
||
- Device tracking and management
|
||
- Enterprise SSO capabilities
|
||
- Custom Identity Provider (IDP) creation
|
||
|
||
**Database Support:**
|
||
- PostgreSQL (via Drizzle or Prisma adapters)
|
||
- MySQL
|
||
- SQLite
|
||
- MongoDB
|
||
- Automatic schema generation via CLI
|
||
- Migration support built-in
|
||
|
||
**Plugin Architecture:**
|
||
- Official plugins: Organization management, 2FA
|
||
- Polar plugin for payments/subscriptions integration
|
||
- Extensible plugin ecosystem
|
||
|
||
**Pricing:**
|
||
- **100% FREE and open-source**
|
||
- No usage limits
|
||
- Self-hosted
|
||
- Full control over data
|
||
|
||
**Pros:**
|
||
- Comprehensive features out-of-the-box (no plugins needed for basics)
|
||
- Excellent TypeScript support with automatic type generation
|
||
- Automatic database schema generation/migration
|
||
- Framework-agnostic design
|
||
- Active development and YC backing
|
||
- Modern developer experience
|
||
- Perfect for monorepo architecture
|
||
- No vendor lock-in
|
||
|
||
**Cons:**
|
||
- Relatively new (2024) - less battle-tested than alternatives
|
||
- Smaller community compared to Auth.js
|
||
- Limited real-world production examples
|
||
- Documentation still growing
|
||
|
||
**Best For:**
|
||
- Modern TypeScript monorepos
|
||
- Teams prioritizing developer experience
|
||
- Projects needing multi-app authentication
|
||
- Cost-sensitive projects (100% free)
|
||
|
||
---
|
||
|
||
### 1.2 Auth.js (NextAuth.js)
|
||
|
||
**Overview:**
|
||
- Evolved from Next.js-specific to framework-agnostic
|
||
- Established open-source project
|
||
- Wide adoption in Next.js ecosystem
|
||
|
||
**Key Features:**
|
||
- 50+ built-in OAuth providers
|
||
- Extensive customization via callbacks
|
||
- JWT or database sessions
|
||
- Multiple database adapters (Prisma, Drizzle, Supabase)
|
||
|
||
**Pricing:**
|
||
- **FREE and open-source**
|
||
- Self-hosted
|
||
|
||
**Pros:**
|
||
- Battle-tested and mature
|
||
- Large community and ecosystem
|
||
- Extensive documentation and examples
|
||
- Great for OAuth provider breadth
|
||
- Works with Supabase via adapter
|
||
|
||
**Cons:**
|
||
- **Maintenance concerns:** Original developer abandoned project, one person maintaining 90% of work
|
||
- Advanced features (2FA) require custom implementation or additional packages
|
||
- More boilerplate for setup
|
||
- Less TypeScript-native than Better Auth
|
||
|
||
**Best For:**
|
||
- Teams already invested in Auth.js
|
||
- Projects heavily using OAuth providers
|
||
- Next.js-first applications
|
||
|
||
---
|
||
|
||
### 1.3 Supabase Auth
|
||
|
||
**Overview:**
|
||
- Integrated auth solution tied to Supabase ecosystem
|
||
- Built-in PostgreSQL integration with RLS
|
||
- Part of the Supabase backend-as-a-service
|
||
|
||
**Key Features:**
|
||
- JWT-based authentication
|
||
- Row-Level Security (RLS) integration
|
||
- OAuth providers
|
||
- Email/password authentication
|
||
- Magic links
|
||
- Phone authentication
|
||
|
||
**Pricing:**
|
||
- Free tier: 50,000 monthly active users
|
||
- Pro: $25/month for 100,000 users
|
||
- Includes PostgreSQL database with RLS
|
||
|
||
**Pros:**
|
||
- Automatic integration with PostgreSQL RLS
|
||
- Well-integrated with Supabase ecosystem
|
||
- Handles database user sync via triggers
|
||
- Good value for price
|
||
- Built-in security features
|
||
|
||
**Cons:**
|
||
- **Critical reliability issues reported:**
|
||
- Random user logouts
|
||
- No configurable session lifetime (fundamental missing feature)
|
||
- Unencrypted client-side token storage
|
||
- No 2FA on their own platform (concerning for security product)
|
||
- Vendor lock-in to Supabase ecosystem
|
||
- Poor documentation quality reported
|
||
- Unresponsive support channels
|
||
- Not ideal for custom auth requirements
|
||
|
||
**Best For:**
|
||
- Projects fully committed to Supabase ecosystem
|
||
- Simple auth requirements
|
||
- Budget-conscious projects with <100k users
|
||
|
||
---
|
||
|
||
### 1.4 Clerk
|
||
|
||
**Overview:**
|
||
- Commercial hosted authentication service
|
||
- Polished UI and developer experience
|
||
|
||
**Key Features:**
|
||
- Pre-built UI components
|
||
- Configurable session lifetime (up to 10 years)
|
||
- Organization management
|
||
- 2FA built-in
|
||
- Excellent Next.js integration
|
||
|
||
**Pricing:**
|
||
- Free tier: 10,000 monthly active users
|
||
- Business plan: **$550/month for 10,000 active users**
|
||
- Scales to $25,000-$60,000+ annually
|
||
|
||
**Pros:**
|
||
- Best-in-class developer experience
|
||
- Beautiful pre-built UI
|
||
- Excellent documentation
|
||
- Managed infrastructure
|
||
- No security expertise required
|
||
|
||
**Cons:**
|
||
- **Prohibitively expensive** for freemium/scaling apps
|
||
- Vendor lock-in
|
||
- Less control over authentication flow
|
||
- Not suitable for multi-app ecosystems with shared auth
|
||
|
||
**Best For:**
|
||
- Well-funded startups with budget
|
||
- Enterprise clients
|
||
- Teams wanting managed solution
|
||
|
||
---
|
||
|
||
### 1.5 Auth0
|
||
|
||
**Overview:**
|
||
- Enterprise identity platform
|
||
- Managed cloud service
|
||
- Comprehensive compliance features
|
||
|
||
**Key Features:**
|
||
- Enterprise SSO
|
||
- SAML integration
|
||
- Risk-based authentication
|
||
- Advanced security features
|
||
- Professional support
|
||
|
||
**Pricing:**
|
||
- Free tier: 25,000 B2C users
|
||
- Essentials: $35/month base + usage
|
||
- Scales to $30,000+ annually for enterprise
|
||
|
||
**Pros:**
|
||
- Enterprise-grade features
|
||
- Compliance certifications
|
||
- Professional support
|
||
- Advanced security capabilities
|
||
|
||
**Cons:**
|
||
- Expensive at scale
|
||
- Vendor lock-in
|
||
- Configuration happens in dashboard vs code
|
||
- Overkill for most projects
|
||
|
||
**Best For:**
|
||
- Large enterprises
|
||
- Projects requiring compliance certifications
|
||
- Organizations needing professional support
|
||
|
||
---
|
||
|
||
### 1.6 Comparison Matrix
|
||
|
||
| Feature | Better Auth | Auth.js | Supabase Auth | Clerk | Auth0 |
|
||
|---------|-------------|---------|---------------|-------|-------|
|
||
| **Pricing** | FREE | FREE | $25/mo (100k users) | $550/mo (10k users) | $35+/mo |
|
||
| **Framework Support** | Universal | Universal | Universal | Next.js-first | Universal |
|
||
| **TypeScript-First** | ✅ Yes | ⚠️ Partial | ⚠️ Partial | ✅ Yes | ⚠️ Partial |
|
||
| **2FA Built-in** | ✅ Yes | ❌ No | ⚠️ Limited | ✅ Yes | ✅ Yes |
|
||
| **Multi-Session** | ✅ Yes | ⚠️ Custom | ⚠️ Limited | ✅ Yes | ✅ Yes |
|
||
| **Multi-Tenancy** | ✅ Yes | ⚠️ Custom | ⚠️ Custom | ✅ Yes | ✅ Yes |
|
||
| **Auto Schema Gen** | ✅ Yes | ❌ No | ✅ Yes | N/A | N/A |
|
||
| **Self-Hosted** | ✅ Yes | ✅ Yes | ⚠️ Hybrid | ❌ No | ❌ No |
|
||
| **PostgreSQL RLS** | ✅ Compatible | ✅ Compatible | ✅ Native | ❌ No | ❌ No |
|
||
| **Monorepo-Friendly** | ✅ Excellent | ⚠️ Good | ⚠️ Good | ⚠️ Limited | ⚠️ Limited |
|
||
| **Vendor Lock-in** | ✅ None | ✅ None | ⚠️ High | ⚠️ High | ⚠️ High |
|
||
| **Maturity** | ⚠️ New (2024) | ✅ Mature | ✅ Mature | ✅ Mature | ✅ Mature |
|
||
| **Maintenance Risk** | ✅ Low | ⚠️ High | ⚠️ Medium | ✅ Low | ✅ Low |
|
||
| **Best For** | Modern monorepos | OAuth-heavy | Supabase projects | Enterprise ($$$) | Enterprise ($$$) |
|
||
|
||
---
|
||
|
||
## 2. PostgreSQL Security Best Practices
|
||
|
||
### 2.1 Authentication Methods
|
||
|
||
#### SCRAM-SHA-256 (Recommended)
|
||
- **Most secure** password-based authentication in PostgreSQL
|
||
- Uses Salted Challenge Response Authentication Mechanism
|
||
- SHA-256 hashing algorithm
|
||
- **Replace MD5 immediately** - MD5 is deprecated and insecure
|
||
|
||
#### Additional Methods
|
||
- **LDAP/PAM:** Centralized authentication for enterprise
|
||
- **Kerberos:** Strong authentication for enterprise environments
|
||
- **Certificate-based:** PKI authentication for services
|
||
- **GSSAPI:** Generic Security Services API support
|
||
|
||
#### Methods to AVOID
|
||
- **Trust authentication:** Never use in production (allows passwordless access)
|
||
- **MD5:** Deprecated, cryptographically broken
|
||
- **Password (plaintext):** Never use
|
||
|
||
---
|
||
|
||
### 2.2 Configuration Hardening
|
||
|
||
#### postgresql.conf
|
||
```sql
|
||
-- Network Configuration
|
||
listen_addresses = '10.0.0.5' -- Specific IP, not '*'
|
||
port = 5432 -- Consider non-standard port
|
||
|
||
-- Authentication
|
||
password_encryption = 'scram-sha-256' -- Force SCRAM
|
||
|
||
-- Connection Limits
|
||
max_connections = 100
|
||
superuser_reserved_connections = 3
|
||
|
||
-- Logging (Security Audit)
|
||
log_connections = on
|
||
log_disconnections = on
|
||
log_duration = on
|
||
log_line_prefix = '%t [%p]: user=%u,db=%d,app=%a,client=%h '
|
||
```
|
||
|
||
#### pg_hba.conf
|
||
```sql
|
||
# TYPE DATABASE USER ADDRESS METHOD
|
||
local all postgres peer
|
||
host all all 10.0.0.0/24 scram-sha-256
|
||
hostssl all all 0.0.0.0/0 scram-sha-256
|
||
|
||
# Reject trust method entirely
|
||
# host all all 127.0.0.1/32 trust # NEVER USE
|
||
```
|
||
|
||
---
|
||
|
||
### 2.3 Access Control Best Practices
|
||
|
||
#### Principle of Least Privilege
|
||
```sql
|
||
-- Create application-specific roles
|
||
CREATE ROLE app_user LOGIN PASSWORD 'strong_password';
|
||
CREATE ROLE app_readonly LOGIN PASSWORD 'strong_password';
|
||
|
||
-- Grant minimal permissions
|
||
GRANT CONNECT ON DATABASE myapp TO app_user;
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE users TO app_user;
|
||
GRANT SELECT ON TABLE users TO app_readonly;
|
||
|
||
-- Revoke public schema access
|
||
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
||
```
|
||
|
||
#### Avoid Shared Accounts
|
||
- Create **unique roles** for each user and application
|
||
- Never share database credentials across services
|
||
- Use different roles for different access levels
|
||
|
||
#### Password Policies
|
||
```sql
|
||
-- Set password expiration
|
||
ALTER ROLE app_user VALID UNTIL '2026-12-31';
|
||
|
||
-- Require strong passwords (enforce at application level)
|
||
-- Mix of uppercase, lowercase, numbers, special characters
|
||
-- Minimum 12-16 characters
|
||
```
|
||
|
||
---
|
||
|
||
### 2.4 Encryption & Transport Security
|
||
|
||
#### SSL/TLS for Connections
|
||
```sql
|
||
-- Enable SSL in postgresql.conf
|
||
ssl = on
|
||
ssl_cert_file = '/path/to/server.crt'
|
||
ssl_key_file = '/path/to/server.key'
|
||
ssl_ca_file = '/path/to/ca.crt'
|
||
|
||
-- Require SSL in pg_hba.conf
|
||
hostssl all all 0.0.0.0/0 scram-sha-256
|
||
```
|
||
|
||
#### Transparent Data Encryption (TDE)
|
||
- Encrypt data at rest using file system or volume encryption
|
||
- Consider pgcrypto extension for column-level encryption
|
||
|
||
---
|
||
|
||
### 2.5 Multi-Factor Authentication
|
||
|
||
PostgreSQL doesn't natively support 2FA, but you can implement it:
|
||
|
||
1. **Network Layer:** Use VPN with MFA requirement
|
||
2. **SSH Tunneling:** SSH with key + password/OTP
|
||
3. **Application Layer:** Implement 2FA in application before database connection
|
||
4. **Identity Provider:** Integrate with LDAP/AD that enforces MFA
|
||
|
||
---
|
||
|
||
### 2.6 Brute Force Protection
|
||
|
||
#### auth_delay Module
|
||
```sql
|
||
-- Install extension
|
||
CREATE EXTENSION auth_delay;
|
||
|
||
-- Adds delay on failed authentication attempts
|
||
-- Configured in postgresql.conf
|
||
auth_delay.milliseconds = 5000 -- 5 second delay
|
||
```
|
||
|
||
#### Connection Pooling Limits
|
||
- Use PgBouncer or similar to limit connection rates
|
||
- Implement application-level rate limiting
|
||
- Monitor failed authentication attempts
|
||
|
||
---
|
||
|
||
### 2.7 Audit & Monitoring
|
||
|
||
#### Essential Logging
|
||
```sql
|
||
-- Enable audit logging
|
||
log_statement = 'ddl' -- or 'all' for comprehensive logging
|
||
log_min_duration_statement = 1000 -- Log slow queries (ms)
|
||
|
||
-- Connection tracking
|
||
log_connections = on
|
||
log_disconnections = on
|
||
|
||
-- Failed attempts
|
||
log_error_verbosity = default
|
||
```
|
||
|
||
#### pgAudit Extension
|
||
```sql
|
||
CREATE EXTENSION pgaudit;
|
||
|
||
-- Configure audit logging
|
||
SET pgaudit.log = 'read, write, ddl';
|
||
SET pgaudit.log_catalog = off;
|
||
SET pgaudit.log_parameter = on;
|
||
```
|
||
|
||
---
|
||
|
||
### 2.8 Row-Level Security (RLS) for Multi-Tenancy
|
||
|
||
**See Section 4 for comprehensive RLS patterns**
|
||
|
||
Key principles:
|
||
- Enable RLS on all multi-tenant tables
|
||
- Create policies for SELECT, INSERT, UPDATE, DELETE
|
||
- Use JWT claims for tenant identification
|
||
- Test policies extensively with automated tests
|
||
|
||
---
|
||
|
||
### 2.9 Security Checklist
|
||
|
||
- [ ] Change default postgres superuser password immediately
|
||
- [ ] Use SCRAM-SHA-256 for all password authentication
|
||
- [ ] Disable trust authentication method
|
||
- [ ] Configure listen_addresses to specific IPs
|
||
- [ ] Enable SSL/TLS for all connections
|
||
- [ ] Create role-specific database users (no sharing)
|
||
- [ ] Apply principle of least privilege to all roles
|
||
- [ ] Enable connection and authentication logging
|
||
- [ ] Install and configure auth_delay extension
|
||
- [ ] Set up automated backups with encryption
|
||
- [ ] Subscribe to PostgreSQL security announcements
|
||
- [ ] Keep PostgreSQL updated to latest stable version
|
||
- [ ] Implement connection pooling with rate limits
|
||
- [ ] Configure pgAudit for compliance requirements
|
||
- [ ] Enable Row-Level Security for multi-tenant tables
|
||
- [ ] Regular security audits and penetration testing
|
||
|
||
---
|
||
|
||
## 3. JWT Security Best Practices
|
||
|
||
### 3.1 Token Expiration Strategy
|
||
|
||
#### Access Tokens
|
||
- **Recommended Lifespan:** 15-30 minutes
|
||
- **Rationale:** Short lifespan limits exposure if compromised
|
||
- **Storage:** Memory or httpOnly cookies (never localStorage)
|
||
|
||
#### Refresh Tokens
|
||
- **Recommended Lifespan:** 7-14 days
|
||
- **Rationale:** Balance between security and user experience
|
||
- **Storage:** httpOnly cookies (web) or secure storage (mobile)
|
||
|
||
```typescript
|
||
// Example token configuration
|
||
const tokenConfig = {
|
||
accessToken: {
|
||
expiresIn: '15m',
|
||
algorithm: 'RS256',
|
||
},
|
||
refreshToken: {
|
||
expiresIn: '7d',
|
||
rotating: true, // Implement token rotation
|
||
},
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 Refresh Token Rotation
|
||
|
||
**Critical Security Feature:**
|
||
- Every refresh token is **single-use only**
|
||
- New refresh token issued with each access token refresh
|
||
- Old refresh token immediately invalidated
|
||
- Detects token theft/replay attacks
|
||
|
||
```typescript
|
||
// Token rotation flow
|
||
async function refreshTokens(refreshToken: string) {
|
||
// 1. Validate refresh token
|
||
const session = await validateRefreshToken(refreshToken);
|
||
|
||
// 2. Check if token already used (replay attack detection)
|
||
if (session.refreshToken !== refreshToken) {
|
||
// Token reuse detected - revoke all user sessions
|
||
await revokeAllUserSessions(session.userId);
|
||
throw new Error('Token reuse detected');
|
||
}
|
||
|
||
// 3. Generate new token pair
|
||
const newAccessToken = generateAccessToken(session.userId);
|
||
const newRefreshToken = generateRefreshToken();
|
||
|
||
// 4. Update session with new refresh token
|
||
await updateSession(session.id, { refreshToken: newRefreshToken });
|
||
|
||
// 5. Return new tokens
|
||
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
|
||
}
|
||
```
|
||
|
||
**Benefits:**
|
||
- Blocks replay attacks
|
||
- Limits blast radius of stolen tokens
|
||
- Simplifies session management
|
||
- Industry standard in 2025
|
||
|
||
---
|
||
|
||
### 3.3 Algorithm Selection
|
||
|
||
#### RS256 (Recommended for Production)
|
||
- **Asymmetric encryption** (public/private key pair)
|
||
- Public key can verify tokens without risk
|
||
- Private key held securely by auth server only
|
||
- **Best for:** Microservices, distributed systems, multi-app ecosystems
|
||
|
||
```typescript
|
||
// RS256 configuration
|
||
const jwtConfig = {
|
||
algorithm: 'RS256',
|
||
privateKey: fs.readFileSync('private.key'),
|
||
publicKey: fs.readFileSync('public.key'),
|
||
issuer: 'manacore-auth',
|
||
audience: ['manacore', 'maerchenzauber', 'memoro', 'picture'],
|
||
};
|
||
```
|
||
|
||
#### HS256 (Acceptable for Monoliths)
|
||
- **Symmetric encryption** (single secret key)
|
||
- Same key for signing and verification
|
||
- Simpler setup
|
||
- **Risk:** Any service with the key can create valid tokens
|
||
|
||
#### NEVER USE
|
||
- **None algorithm:** Allows unsigned tokens (security disaster)
|
||
- **Weak algorithms:** HS256 with weak secrets
|
||
|
||
---
|
||
|
||
### 3.4 Claims Validation
|
||
|
||
#### Standard Claims (MUST Validate)
|
||
```typescript
|
||
interface StandardClaims {
|
||
iss: string; // Issuer - must match your auth server
|
||
sub: string; // Subject - user ID
|
||
aud: string | string[]; // Audience - target application(s)
|
||
exp: number; // Expiration time - MUST validate
|
||
iat: number; // Issued at - detect old tokens
|
||
nbf?: number; // Not before - prevent premature use
|
||
jti?: string; // JWT ID - for revocation tracking
|
||
}
|
||
|
||
// Validation example
|
||
function validateToken(token: string) {
|
||
const decoded = jwt.verify(token, publicKey, {
|
||
algorithms: ['RS256'],
|
||
issuer: 'manacore-auth',
|
||
audience: currentApp,
|
||
clockTolerance: 30, // 30 seconds for clock skew
|
||
});
|
||
|
||
// Additional validation
|
||
if (decoded.iat && decoded.iat > Date.now() / 1000) {
|
||
throw new Error('Token issued in the future');
|
||
}
|
||
|
||
return decoded;
|
||
}
|
||
```
|
||
|
||
#### Custom Claims Best Practices
|
||
|
||
**The 90/10 Rule:**
|
||
- Only include **frequently used claims** (90% of requests need them)
|
||
- Keep claims minimal to reduce token size
|
||
- Avoid sensitive data in JWT payload
|
||
|
||
```typescript
|
||
// Good: Minimal custom claims
|
||
interface AppClaims extends StandardClaims {
|
||
app_id: string; // Which app is this for?
|
||
role: 'user' | 'admin'; // User role
|
||
tenant_id?: string; // For multi-tenant apps
|
||
}
|
||
|
||
// Bad: Too much data
|
||
interface BadClaims extends StandardClaims {
|
||
email: string; // PII - not needed in most requests
|
||
full_name: string; // PII - fetch from database when needed
|
||
address: object; // Large object - bloats token
|
||
preferences: object; // Rarely needed - fetch separately
|
||
profile_pic: string; // Large data - serve via CDN
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.5 app_metadata vs user_metadata
|
||
|
||
**Pattern from Auth0/Supabase:**
|
||
|
||
```typescript
|
||
// app_metadata: System-controlled, security-sensitive
|
||
interface AppMetadata {
|
||
role: 'user' | 'admin' | 'moderator';
|
||
tenant_id: string;
|
||
subscription: 'free' | 'pro' | 'enterprise';
|
||
credits: number;
|
||
flags: string[]; // Feature flags
|
||
}
|
||
|
||
// user_metadata: User-controlled, non-sensitive
|
||
interface UserMetadata {
|
||
display_name: string;
|
||
avatar_url: string;
|
||
preferences: {
|
||
theme: 'light' | 'dark';
|
||
language: string;
|
||
};
|
||
}
|
||
|
||
// In JWT, only include security-critical app_metadata
|
||
interface JWTPayload {
|
||
sub: string;
|
||
app_metadata: {
|
||
role: string;
|
||
tenant_id: string;
|
||
};
|
||
// user_metadata fetched from database when needed
|
||
}
|
||
```
|
||
|
||
**Namespacing Custom Claims:**
|
||
```typescript
|
||
// Use namespaced claims to avoid conflicts
|
||
interface NamespacedClaims {
|
||
'https://manacore.ai/app_id': string;
|
||
'https://manacore.ai/role': string;
|
||
'https://manacore.ai/tenant_id': string;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.6 Storage Best Practices
|
||
|
||
#### ❌ NEVER Store in localStorage
|
||
```typescript
|
||
// VULNERABLE TO XSS ATTACKS
|
||
localStorage.setItem('token', accessToken); // DON'T DO THIS
|
||
```
|
||
|
||
#### ✅ Web Applications
|
||
```typescript
|
||
// Option 1: httpOnly cookies (best for web)
|
||
res.cookie('accessToken', token, {
|
||
httpOnly: true, // Not accessible via JavaScript
|
||
secure: true, // HTTPS only
|
||
sameSite: 'strict', // CSRF protection
|
||
maxAge: 15 * 60 * 1000, // 15 minutes
|
||
});
|
||
|
||
// Option 2: Memory only (for SPA)
|
||
// Store in closure or React state, lost on refresh
|
||
let accessToken = null;
|
||
```
|
||
|
||
#### ✅ Mobile Applications
|
||
```typescript
|
||
// Expo SecureStore (encrypted storage)
|
||
import * as SecureStore from 'expo-secure-store';
|
||
|
||
await SecureStore.setItemAsync('accessToken', token);
|
||
const token = await SecureStore.getItemAsync('accessToken');
|
||
```
|
||
|
||
---
|
||
|
||
### 3.7 Transport Security
|
||
|
||
```typescript
|
||
// ONLY send tokens over HTTPS
|
||
if (window.location.protocol !== 'https:' && !isDevelopment) {
|
||
throw new Error('JWT must be transmitted over HTTPS');
|
||
}
|
||
|
||
// Include in Authorization header
|
||
const headers = {
|
||
'Authorization': `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json',
|
||
};
|
||
|
||
// NEVER include in URL query parameters
|
||
// ❌ BAD: https://api.example.com/users?token=eyJ...
|
||
// ✅ GOOD: Authorization: Bearer eyJ...
|
||
```
|
||
|
||
---
|
||
|
||
### 3.8 Token Revocation Strategy
|
||
|
||
JWTs are stateless, making revocation challenging. Solutions:
|
||
|
||
#### 1. Short Expiration + Refresh Tokens
|
||
- Access tokens expire quickly (15min)
|
||
- Revoke refresh tokens in database
|
||
- Compromised access token only valid briefly
|
||
|
||
#### 2. Token Blacklist (jti claim)
|
||
```typescript
|
||
// Add unique ID to each token
|
||
const token = jwt.sign(
|
||
{ sub: userId, jti: uuid() },
|
||
privateKey
|
||
);
|
||
|
||
// Check blacklist on each request
|
||
async function validateToken(token: string) {
|
||
const decoded = jwt.verify(token, publicKey);
|
||
|
||
const isBlacklisted = await redis.exists(`blacklist:${decoded.jti}`);
|
||
if (isBlacklisted) {
|
||
throw new Error('Token revoked');
|
||
}
|
||
|
||
return decoded;
|
||
}
|
||
|
||
// Revoke token
|
||
async function revokeToken(jti: string, expiresAt: Date) {
|
||
const ttl = Math.floor((expiresAt.getTime() - Date.now()) / 1000);
|
||
await redis.setex(`blacklist:${jti}`, ttl, '1');
|
||
}
|
||
```
|
||
|
||
#### 3. Session Table (Hybrid Approach)
|
||
```sql
|
||
CREATE TABLE sessions (
|
||
id UUID PRIMARY KEY,
|
||
user_id UUID NOT NULL,
|
||
refresh_token TEXT NOT NULL,
|
||
access_token_jti TEXT,
|
||
device_info JSONB,
|
||
ip_address INET,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
last_used_at TIMESTAMP DEFAULT NOW(),
|
||
expires_at TIMESTAMP NOT NULL,
|
||
revoked BOOLEAN DEFAULT FALSE
|
||
);
|
||
|
||
-- Quick revocation check
|
||
CREATE INDEX idx_sessions_jti ON sessions(access_token_jti) WHERE revoked = FALSE;
|
||
```
|
||
|
||
---
|
||
|
||
### 3.9 Security Checklist
|
||
|
||
- [ ] Use RS256 algorithm for distributed systems
|
||
- [ ] Set access token expiration to 15-30 minutes
|
||
- [ ] Implement refresh token rotation
|
||
- [ ] Validate all standard claims (iss, aud, exp, iat, nbf)
|
||
- [ ] Store tokens in httpOnly cookies (web) or secure storage (mobile)
|
||
- [ ] NEVER store tokens in localStorage
|
||
- [ ] Transmit tokens only over HTTPS
|
||
- [ ] Keep JWT payload minimal (90/10 rule)
|
||
- [ ] Namespace custom claims to avoid conflicts
|
||
- [ ] Implement token revocation strategy
|
||
- [ ] Monitor for token reuse (replay attacks)
|
||
- [ ] Include device fingerprinting for suspicious activity
|
||
- [ ] Set up automated token cleanup/expiration
|
||
|
||
---
|
||
|
||
## 4. PostgreSQL Row-Level Security (RLS) for Multi-Tenancy
|
||
|
||
### 4.1 Overview
|
||
|
||
**Row-Level Security (RLS)** is a PostgreSQL feature (since 9.5) that allows fine-grained access control at the row level. It's essential for multi-tenant SaaS applications.
|
||
|
||
**Key Benefit:** Defense in depth - even if application code has bugs, database won't return data outside tenant scope.
|
||
|
||
---
|
||
|
||
### 4.2 Basic RLS Setup
|
||
|
||
```sql
|
||
-- 1. Create multi-tenant table
|
||
CREATE TABLE posts (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID NOT NULL,
|
||
user_id UUID NOT NULL,
|
||
title TEXT NOT NULL,
|
||
content TEXT,
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
|
||
-- 2. Enable RLS on table
|
||
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
||
|
||
-- 3. Create policy for SELECT
|
||
CREATE POLICY tenant_isolation_select ON posts
|
||
FOR SELECT
|
||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||
|
||
-- 4. Create policy for INSERT
|
||
CREATE POLICY tenant_isolation_insert ON posts
|
||
FOR INSERT
|
||
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||
|
||
-- 5. Create policy for UPDATE
|
||
CREATE POLICY tenant_isolation_update ON posts
|
||
FOR UPDATE
|
||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID)
|
||
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||
|
||
-- 6. Create policy for DELETE
|
||
CREATE POLICY tenant_isolation_delete ON posts
|
||
FOR DELETE
|
||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.3 Setting Tenant Context from JWT
|
||
|
||
```typescript
|
||
// Application code (NestJS example)
|
||
export class TenantMiddleware implements NestMiddleware {
|
||
async use(req: Request, res: Response, next: NextFunction) {
|
||
// 1. Extract JWT from request
|
||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
// 2. Verify and decode JWT
|
||
const decoded = jwt.verify(token, publicKey);
|
||
|
||
// 3. Set tenant context for database queries
|
||
await this.dataSource.query(
|
||
`SET LOCAL app.current_tenant_id = $1`,
|
||
[decoded.tenant_id]
|
||
);
|
||
|
||
// 4. Store in request for application use
|
||
req.tenantId = decoded.tenant_id;
|
||
req.userId = decoded.sub;
|
||
|
||
next();
|
||
}
|
||
}
|
||
|
||
// Alternative: Set at connection level for entire session
|
||
async function createTenantConnection(tenantId: string) {
|
||
const connection = await pool.connect();
|
||
await connection.query(`SET app.current_tenant_id = $1`, [tenantId]);
|
||
return connection;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.4 RLS with Supabase Integration
|
||
|
||
```sql
|
||
-- Supabase automatically sets auth.uid() from JWT
|
||
-- Use auth.uid() for user-level isolation
|
||
CREATE POLICY user_own_data ON posts
|
||
FOR ALL
|
||
USING (user_id = auth.uid());
|
||
|
||
-- Combine with tenant isolation
|
||
CREATE POLICY tenant_and_user_isolation ON posts
|
||
FOR ALL
|
||
USING (
|
||
tenant_id = current_setting('app.current_tenant_id', true)::UUID
|
||
AND user_id = auth.uid()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.5 Advanced RLS Patterns
|
||
|
||
#### Role-Based Access Control (RBAC)
|
||
```sql
|
||
-- Create roles table
|
||
CREATE TABLE tenant_users (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID NOT NULL,
|
||
user_id UUID NOT NULL,
|
||
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
|
||
UNIQUE(tenant_id, user_id)
|
||
);
|
||
|
||
-- Enable RLS on tenant_users
|
||
ALTER TABLE tenant_users ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE POLICY tenant_users_select ON tenant_users
|
||
FOR SELECT
|
||
USING (user_id = auth.uid());
|
||
|
||
-- Policy allowing admins to see all posts, members only their own
|
||
CREATE POLICY posts_rbac ON posts
|
||
FOR SELECT
|
||
USING (
|
||
tenant_id = current_setting('app.current_tenant_id', true)::UUID
|
||
AND (
|
||
-- User is admin or owner
|
||
EXISTS (
|
||
SELECT 1 FROM tenant_users
|
||
WHERE tenant_id = posts.tenant_id
|
||
AND user_id = auth.uid()
|
||
AND role IN ('owner', 'admin')
|
||
)
|
||
-- OR user owns the post
|
||
OR user_id = auth.uid()
|
||
)
|
||
);
|
||
```
|
||
|
||
#### Shared Resources Across Tenants
|
||
```sql
|
||
-- Some resources might be shared (e.g., public templates)
|
||
CREATE TABLE templates (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
tenant_id UUID, -- NULL for public templates
|
||
name TEXT NOT NULL,
|
||
is_public BOOLEAN DEFAULT FALSE
|
||
);
|
||
|
||
CREATE POLICY templates_access ON templates
|
||
FOR SELECT
|
||
USING (
|
||
is_public = TRUE -- Public templates accessible to all
|
||
OR tenant_id = current_setting('app.current_tenant_id', true)::UUID
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.6 Performance Considerations
|
||
|
||
#### Indexing for RLS
|
||
```sql
|
||
-- CRITICAL: Index tenant_id for RLS performance
|
||
CREATE INDEX idx_posts_tenant_id ON posts(tenant_id);
|
||
|
||
-- Composite indexes for common query patterns
|
||
CREATE INDEX idx_posts_tenant_user ON posts(tenant_id, user_id);
|
||
CREATE INDEX idx_posts_tenant_created ON posts(tenant_id, created_at DESC);
|
||
```
|
||
|
||
#### Policy Combining (OR vs AND)
|
||
```sql
|
||
-- By default, multiple policies are combined with OR
|
||
-- Policy 1: User can see their own posts
|
||
CREATE POLICY user_own_posts ON posts
|
||
FOR SELECT
|
||
USING (user_id = auth.uid());
|
||
|
||
-- Policy 2: Admins can see all tenant posts
|
||
CREATE POLICY admin_all_posts ON posts
|
||
FOR SELECT
|
||
USING (
|
||
EXISTS (
|
||
SELECT 1 FROM tenant_users
|
||
WHERE tenant_id = posts.tenant_id
|
||
AND user_id = auth.uid()
|
||
AND role = 'admin'
|
||
)
|
||
);
|
||
|
||
-- Result: User sees their posts OR all posts if admin (OR logic)
|
||
```
|
||
|
||
---
|
||
|
||
### 4.7 Testing RLS Policies
|
||
|
||
**CRITICAL:** Extensively test RLS policies to prevent data leaks.
|
||
|
||
```typescript
|
||
// Integration test example
|
||
describe('RLS Tenant Isolation', () => {
|
||
it('should not allow user from tenant A to see tenant B data', async () => {
|
||
// Create data for tenant A
|
||
const tenantAUser = await createUser({ tenantId: 'tenant-a' });
|
||
const tenantAPost = await createPost({
|
||
tenantId: 'tenant-a',
|
||
userId: tenantAUser.id
|
||
});
|
||
|
||
// Create user for tenant B
|
||
const tenantBUser = await createUser({ tenantId: 'tenant-b' });
|
||
|
||
// Authenticate as tenant B user
|
||
const tenantBToken = generateToken(tenantBUser);
|
||
|
||
// Attempt to access tenant A data
|
||
const response = await request(app)
|
||
.get(`/posts/${tenantAPost.id}`)
|
||
.set('Authorization', `Bearer ${tenantBToken}`);
|
||
|
||
// Should be forbidden or not found
|
||
expect(response.status).toBe(404); // Or 403
|
||
});
|
||
|
||
it('should allow admin to see all tenant posts', async () => {
|
||
const admin = await createUser({ tenantId: 'tenant-a', role: 'admin' });
|
||
const member = await createUser({ tenantId: 'tenant-a', role: 'member' });
|
||
const memberPost = await createPost({
|
||
tenantId: 'tenant-a',
|
||
userId: member.id
|
||
});
|
||
|
||
const adminToken = generateToken(admin);
|
||
const response = await request(app)
|
||
.get(`/posts/${memberPost.id}`)
|
||
.set('Authorization', `Bearer ${adminToken}`);
|
||
|
||
expect(response.status).toBe(200);
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 4.8 Views and RLS Bypass Risk
|
||
|
||
**CRITICAL SECURITY WARNING:**
|
||
|
||
```sql
|
||
-- Views can bypass RLS if owned by superuser!
|
||
CREATE VIEW all_posts AS SELECT * FROM posts;
|
||
|
||
-- If view owner has BYPASSRLS privilege, RLS is ignored
|
||
ALTER VIEW all_posts OWNER TO postgres; -- DANGEROUS
|
||
|
||
-- Solution: Views should be owned by role without BYPASSRLS
|
||
CREATE ROLE app_viewer;
|
||
ALTER VIEW all_posts OWNER TO app_viewer;
|
||
|
||
-- Or use security barrier views
|
||
CREATE VIEW safe_posts WITH (security_barrier) AS
|
||
SELECT * FROM posts;
|
||
```
|
||
|
||
---
|
||
|
||
### 4.9 RLS Best Practices
|
||
|
||
- [ ] Enable RLS on ALL multi-tenant tables
|
||
- [ ] Create policies for SELECT, INSERT, UPDATE, DELETE separately
|
||
- [ ] Index tenant_id columns for performance
|
||
- [ ] Use runtime configuration variables for tenant context
|
||
- [ ] Combine RLS with application-level validation (defense in depth)
|
||
- [ ] Test policies extensively with integration tests
|
||
- [ ] Avoid views owned by superuser (bypasses RLS)
|
||
- [ ] Use security barrier views when needed
|
||
- [ ] Monitor RLS policy performance
|
||
- [ ] Document policies and their intended behavior
|
||
- [ ] Regular security audits of RLS policies
|
||
- [ ] Use connection pooling with proper tenant context setting
|
||
|
||
---
|
||
|
||
## 5. Credit/Token System Architecture
|
||
|
||
### 5.1 Core Requirements
|
||
|
||
A robust credit system for multi-app ecosystems needs:
|
||
- **Accuracy:** Financial precision (no rounding errors)
|
||
- **Auditability:** Complete transaction history
|
||
- **Idempotency:** No duplicate charges
|
||
- **Atomicity:** All-or-nothing transactions
|
||
- **Performance:** Fast balance checks
|
||
- **Security:** Prevent unauthorized access
|
||
- **Scalability:** Handle high transaction volume
|
||
|
||
---
|
||
|
||
### 5.2 Database Schema - Ledger Pattern
|
||
|
||
**Double-Entry Accounting** is the gold standard for financial systems.
|
||
|
||
```sql
|
||
-- 1. Accounts table (user wallets)
|
||
CREATE TABLE accounts (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||
account_type TEXT NOT NULL CHECK (account_type IN ('credit', 'bonus', 'pending')),
|
||
balance DECIMAL(20, 2) NOT NULL DEFAULT 0 CHECK (balance >= 0),
|
||
currency TEXT NOT NULL DEFAULT 'USD',
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW(),
|
||
UNIQUE(user_id, account_type, currency)
|
||
);
|
||
|
||
-- 2. Transactions table (ledger)
|
||
CREATE TABLE transactions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||
idempotency_key TEXT UNIQUE NOT NULL, -- Critical for preventing duplicates
|
||
transaction_type TEXT NOT NULL CHECK (
|
||
transaction_type IN (
|
||
'credit_purchase', 'credit_usage', 'bonus_grant',
|
||
'refund', 'adjustment', 'expiration'
|
||
)
|
||
),
|
||
amount DECIMAL(20, 2) NOT NULL,
|
||
currency TEXT NOT NULL DEFAULT 'USD',
|
||
status TEXT NOT NULL DEFAULT 'pending' CHECK (
|
||
status IN ('pending', 'completed', 'failed', 'reversed')
|
||
),
|
||
metadata JSONB, -- Store app_id, feature_id, stripe_payment_id, etc.
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
completed_at TIMESTAMP,
|
||
expires_at TIMESTAMP,
|
||
|
||
-- Double-entry references
|
||
debit_account_id UUID REFERENCES accounts(id),
|
||
credit_account_id UUID REFERENCES accounts(id),
|
||
|
||
-- Audit trail
|
||
created_by UUID REFERENCES auth.users(id),
|
||
notes TEXT
|
||
);
|
||
|
||
-- 3. Transaction entries (double-entry records)
|
||
CREATE TABLE transaction_entries (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
transaction_id UUID NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
|
||
account_id UUID NOT NULL REFERENCES accounts(id),
|
||
entry_type TEXT NOT NULL CHECK (entry_type IN ('debit', 'credit')),
|
||
amount DECIMAL(20, 2) NOT NULL,
|
||
balance_after DECIMAL(20, 2) NOT NULL, -- Snapshot for auditing
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
|
||
-- 4. Indexes
|
||
CREATE INDEX idx_accounts_user ON accounts(user_id);
|
||
CREATE INDEX idx_transactions_user ON transactions(user_id);
|
||
CREATE INDEX idx_transactions_status ON transactions(status);
|
||
CREATE INDEX idx_transactions_idempotency ON transactions(idempotency_key);
|
||
CREATE INDEX idx_entries_transaction ON transaction_entries(transaction_id);
|
||
CREATE INDEX idx_entries_account ON transaction_entries(account_id);
|
||
```
|
||
|
||
---
|
||
|
||
### 5.3 Idempotency Implementation
|
||
|
||
**Critical for preventing duplicate charges:**
|
||
|
||
```typescript
|
||
// Client generates idempotency key
|
||
import { v4 as uuidv4 } from 'uuid';
|
||
|
||
async function purchaseCredits(userId: string, amount: number) {
|
||
const idempotencyKey = uuidv4(); // Or use request ID
|
||
|
||
return await fetch('/api/credits/purchase', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Idempotency-Key': idempotencyKey,
|
||
},
|
||
body: JSON.stringify({ amount }),
|
||
});
|
||
}
|
||
|
||
// Server-side handler
|
||
async function handleCreditPurchase(
|
||
userId: string,
|
||
amount: number,
|
||
idempotencyKey: string
|
||
) {
|
||
// 1. Check if transaction already exists
|
||
const existing = await db.query(
|
||
'SELECT * FROM transactions WHERE idempotency_key = $1',
|
||
[idempotencyKey]
|
||
);
|
||
|
||
if (existing.rows.length > 0) {
|
||
// Transaction already processed - return existing result
|
||
return {
|
||
status: existing.rows[0].status,
|
||
transactionId: existing.rows[0].id,
|
||
cached: true,
|
||
};
|
||
}
|
||
|
||
// 2. Begin database transaction
|
||
await db.query('BEGIN');
|
||
|
||
try {
|
||
// 3. Create transaction record (locks via idempotency_key UNIQUE)
|
||
const transaction = await db.query(
|
||
`INSERT INTO transactions (
|
||
user_id, idempotency_key, transaction_type,
|
||
amount, status
|
||
) VALUES ($1, $2, 'credit_purchase', $3, 'pending')
|
||
RETURNING *`,
|
||
[userId, idempotencyKey, amount]
|
||
);
|
||
|
||
// 4. Process Stripe payment
|
||
const payment = await stripe.paymentIntents.create({
|
||
amount: amount * 100, // Stripe uses cents
|
||
currency: 'usd',
|
||
metadata: {
|
||
transaction_id: transaction.rows[0].id,
|
||
user_id: userId,
|
||
},
|
||
});
|
||
|
||
// 5. Update transaction status
|
||
await db.query(
|
||
'UPDATE transactions SET status = $1, completed_at = NOW() WHERE id = $2',
|
||
['completed', transaction.rows[0].id]
|
||
);
|
||
|
||
// 6. Credit user account
|
||
await creditUserAccount(userId, amount, transaction.rows[0].id);
|
||
|
||
// 7. Commit
|
||
await db.query('COMMIT');
|
||
|
||
return {
|
||
status: 'completed',
|
||
transactionId: transaction.rows[0].id,
|
||
};
|
||
|
||
} catch (error) {
|
||
// 8. Rollback on error
|
||
await db.query('ROLLBACK');
|
||
|
||
// Mark transaction as failed
|
||
await db.query(
|
||
'UPDATE transactions SET status = $1 WHERE idempotency_key = $2',
|
||
['failed', idempotencyKey]
|
||
);
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5.4 Double-Entry Bookkeeping
|
||
|
||
```typescript
|
||
// Credit user account (double-entry)
|
||
async function creditUserAccount(
|
||
userId: string,
|
||
amount: number,
|
||
transactionId: string
|
||
) {
|
||
await db.query('BEGIN');
|
||
|
||
try {
|
||
// 1. Get or create user credit account
|
||
const account = await db.query(
|
||
`INSERT INTO accounts (user_id, account_type, balance)
|
||
VALUES ($1, 'credit', 0)
|
||
ON CONFLICT (user_id, account_type, currency)
|
||
DO UPDATE SET updated_at = NOW()
|
||
RETURNING *`,
|
||
[userId]
|
||
);
|
||
|
||
// 2. Lock account for update
|
||
await db.query(
|
||
'SELECT * FROM accounts WHERE id = $1 FOR UPDATE',
|
||
[account.rows[0].id]
|
||
);
|
||
|
||
// 3. Update balance
|
||
const newBalance = await db.query(
|
||
`UPDATE accounts
|
||
SET balance = balance + $1, updated_at = NOW()
|
||
WHERE id = $2
|
||
RETURNING balance`,
|
||
[amount, account.rows[0].id]
|
||
);
|
||
|
||
// 4. Create credit entry
|
||
await db.query(
|
||
`INSERT INTO transaction_entries (
|
||
transaction_id, account_id, entry_type, amount, balance_after
|
||
) VALUES ($1, $2, 'credit', $3, $4)`,
|
||
[transactionId, account.rows[0].id, amount, newBalance.rows[0].balance]
|
||
);
|
||
|
||
// 5. Create corresponding debit entry (from system account)
|
||
const systemAccount = await getSystemAccount();
|
||
await db.query(
|
||
`INSERT INTO transaction_entries (
|
||
transaction_id, account_id, entry_type, amount, balance_after
|
||
) VALUES ($1, $2, 'debit', $3, 0)`,
|
||
[transactionId, systemAccount.id, amount]
|
||
);
|
||
|
||
await db.query('COMMIT');
|
||
|
||
return newBalance.rows[0].balance;
|
||
|
||
} catch (error) {
|
||
await db.query('ROLLBACK');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Debit user account (use credits)
|
||
async function debitUserAccount(
|
||
userId: string,
|
||
amount: number,
|
||
appId: string,
|
||
featureId: string
|
||
) {
|
||
const idempotencyKey = `${userId}-${appId}-${featureId}-${Date.now()}`;
|
||
|
||
await db.query('BEGIN');
|
||
|
||
try {
|
||
// 1. Check sufficient balance
|
||
const account = await db.query(
|
||
'SELECT * FROM accounts WHERE user_id = $1 AND account_type = $2 FOR UPDATE',
|
||
[userId, 'credit']
|
||
);
|
||
|
||
if (!account.rows.length || account.rows[0].balance < amount) {
|
||
throw new Error('Insufficient credits');
|
||
}
|
||
|
||
// 2. Create transaction
|
||
const transaction = await db.query(
|
||
`INSERT INTO transactions (
|
||
user_id, idempotency_key, transaction_type, amount,
|
||
status, metadata
|
||
) VALUES ($1, $2, 'credit_usage', $3, 'completed', $4)
|
||
RETURNING *`,
|
||
[
|
||
userId,
|
||
idempotencyKey,
|
||
amount,
|
||
JSON.stringify({ app_id: appId, feature_id: featureId })
|
||
]
|
||
);
|
||
|
||
// 3. Debit account
|
||
const newBalance = await db.query(
|
||
`UPDATE accounts
|
||
SET balance = balance - $1, updated_at = NOW()
|
||
WHERE id = $2
|
||
RETURNING balance`,
|
||
[amount, account.rows[0].id]
|
||
);
|
||
|
||
// 4. Create debit entry
|
||
await db.query(
|
||
`INSERT INTO transaction_entries (
|
||
transaction_id, account_id, entry_type, amount, balance_after
|
||
) VALUES ($1, $2, 'debit', $3, $4)`,
|
||
[transaction.rows[0].id, account.rows[0].id, amount, newBalance.rows[0].balance]
|
||
);
|
||
|
||
await db.query('COMMIT');
|
||
|
||
return {
|
||
success: true,
|
||
newBalance: newBalance.rows[0].balance,
|
||
transactionId: transaction.rows[0].id,
|
||
};
|
||
|
||
} catch (error) {
|
||
await db.query('ROLLBACK');
|
||
throw error;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5.5 Balance Calculation Strategies
|
||
|
||
#### Strategy 1: Cached Balance (Recommended)
|
||
```sql
|
||
-- Store balance in accounts table
|
||
-- Update on each transaction
|
||
-- Fast reads, consistent with double-entry
|
||
|
||
SELECT balance FROM accounts
|
||
WHERE user_id = $1 AND account_type = 'credit';
|
||
```
|
||
|
||
#### Strategy 2: Calculated Balance
|
||
```sql
|
||
-- Calculate from transaction entries
|
||
-- Slower but always accurate
|
||
-- Good for auditing
|
||
|
||
SELECT
|
||
COALESCE(SUM(
|
||
CASE
|
||
WHEN entry_type = 'credit' THEN amount
|
||
WHEN entry_type = 'debit' THEN -amount
|
||
END
|
||
), 0) as balance
|
||
FROM transaction_entries te
|
||
JOIN transactions t ON te.transaction_id = t.id
|
||
WHERE t.user_id = $1 AND t.status = 'completed';
|
||
```
|
||
|
||
#### Strategy 3: Hybrid (Best Practice)
|
||
```typescript
|
||
// Use cached balance for speed
|
||
// Periodically verify against calculated balance
|
||
async function verifyAccountBalance(userId: string) {
|
||
const cached = await getCachedBalance(userId);
|
||
const calculated = await calculateBalance(userId);
|
||
|
||
if (Math.abs(cached - calculated) > 0.01) {
|
||
// Balance mismatch - alert and reconcile
|
||
await alertBalanceMismatch(userId, cached, calculated);
|
||
await reconcileBalance(userId, calculated);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5.6 Credit Types & Expiration
|
||
|
||
```sql
|
||
-- Support multiple credit types
|
||
CREATE TABLE credit_types (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
expires_after INTERVAL, -- e.g., '90 days'
|
||
priority INTEGER DEFAULT 0, -- Lower priority used first
|
||
can_refund BOOLEAN DEFAULT TRUE
|
||
);
|
||
|
||
-- Track expiration per credit batch
|
||
CREATE TABLE credit_balances (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||
credit_type_id UUID NOT NULL REFERENCES credit_types(id),
|
||
amount DECIMAL(20, 2) NOT NULL,
|
||
remaining DECIMAL(20, 2) NOT NULL,
|
||
granted_at TIMESTAMP DEFAULT NOW(),
|
||
expires_at TIMESTAMP,
|
||
transaction_id UUID REFERENCES transactions(id)
|
||
);
|
||
|
||
-- Use credits with expiration priority
|
||
CREATE FUNCTION use_credits(
|
||
p_user_id UUID,
|
||
p_amount DECIMAL
|
||
) RETURNS VOID AS $$
|
||
DECLARE
|
||
v_credit_balance RECORD;
|
||
v_remaining DECIMAL := p_amount;
|
||
v_to_deduct DECIMAL;
|
||
BEGIN
|
||
-- Use credits in priority order (expiring first)
|
||
FOR v_credit_balance IN
|
||
SELECT * FROM credit_balances
|
||
WHERE user_id = p_user_id
|
||
AND remaining > 0
|
||
AND (expires_at IS NULL OR expires_at > NOW())
|
||
ORDER BY
|
||
CASE WHEN expires_at IS NULL THEN 1 ELSE 0 END, -- Non-expiring last
|
||
expires_at ASC, -- Expiring soon first
|
||
granted_at ASC -- Older first
|
||
FOR UPDATE
|
||
LOOP
|
||
EXIT WHEN v_remaining <= 0;
|
||
|
||
v_to_deduct := LEAST(v_credit_balance.remaining, v_remaining);
|
||
|
||
UPDATE credit_balances
|
||
SET remaining = remaining - v_to_deduct
|
||
WHERE id = v_credit_balance.id;
|
||
|
||
v_remaining := v_remaining - v_to_deduct;
|
||
END LOOP;
|
||
|
||
IF v_remaining > 0 THEN
|
||
RAISE EXCEPTION 'Insufficient credits';
|
||
END IF;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
```
|
||
|
||
---
|
||
|
||
### 5.7 Refunds & Reversals
|
||
|
||
```typescript
|
||
async function refundTransaction(
|
||
transactionId: string,
|
||
reason: string,
|
||
partialAmount?: number
|
||
) {
|
||
await db.query('BEGIN');
|
||
|
||
try {
|
||
// 1. Get original transaction
|
||
const original = await db.query(
|
||
'SELECT * FROM transactions WHERE id = $1 FOR UPDATE',
|
||
[transactionId]
|
||
);
|
||
|
||
if (!original.rows.length) {
|
||
throw new Error('Transaction not found');
|
||
}
|
||
|
||
if (original.rows[0].status !== 'completed') {
|
||
throw new Error('Can only refund completed transactions');
|
||
}
|
||
|
||
const refundAmount = partialAmount || original.rows[0].amount;
|
||
|
||
// 2. Process Stripe refund
|
||
if (original.rows[0].metadata.stripe_payment_id) {
|
||
await stripe.refunds.create({
|
||
payment_intent: original.rows[0].metadata.stripe_payment_id,
|
||
amount: Math.round(refundAmount * 100),
|
||
reason: 'requested_by_customer',
|
||
});
|
||
}
|
||
|
||
// 3. Create reversal transaction
|
||
const reversal = await db.query(
|
||
`INSERT INTO transactions (
|
||
user_id, idempotency_key, transaction_type, amount,
|
||
status, metadata
|
||
) VALUES ($1, $2, 'refund', $3, 'completed', $4)
|
||
RETURNING *`,
|
||
[
|
||
original.rows[0].user_id,
|
||
`refund-${transactionId}-${Date.now()}`,
|
||
refundAmount,
|
||
JSON.stringify({
|
||
original_transaction_id: transactionId,
|
||
reason
|
||
})
|
||
]
|
||
);
|
||
|
||
// 4. Reverse account entries
|
||
await debitUserAccount(
|
||
original.rows[0].user_id,
|
||
refundAmount,
|
||
reversal.rows[0].id
|
||
);
|
||
|
||
// 5. Mark original transaction as reversed
|
||
await db.query(
|
||
`UPDATE transactions
|
||
SET status = 'reversed',
|
||
metadata = metadata || $1
|
||
WHERE id = $2`,
|
||
[
|
||
JSON.stringify({ reversal_transaction_id: reversal.rows[0].id }),
|
||
transactionId
|
||
]
|
||
);
|
||
|
||
await db.query('COMMIT');
|
||
|
||
return reversal.rows[0];
|
||
|
||
} catch (error) {
|
||
await db.query('ROLLBACK');
|
||
throw error;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5.8 Credit System Best Practices
|
||
|
||
- [ ] Use DECIMAL for monetary values (never FLOAT)
|
||
- [ ] Implement idempotency for all financial transactions
|
||
- [ ] Use database transactions (BEGIN/COMMIT/ROLLBACK)
|
||
- [ ] Lock accounts during balance updates (SELECT FOR UPDATE)
|
||
- [ ] Store complete audit trail in transaction_entries
|
||
- [ ] Implement double-entry bookkeeping pattern
|
||
- [ ] Support multiple credit types (paid, bonus, promotional)
|
||
- [ ] Handle credit expiration gracefully
|
||
- [ ] Provide refund/reversal capabilities
|
||
- [ ] Monitor balance consistency (cached vs calculated)
|
||
- [ ] Use meaningful idempotency keys
|
||
- [ ] Store rich metadata (app_id, feature_id, user context)
|
||
- [ ] Implement spending limits and rate limiting
|
||
- [ ] Provide detailed transaction history API
|
||
- [ ] Set up alerts for unusual activity
|
||
- [ ] Regular reconciliation and auditing
|
||
|
||
---
|
||
|
||
## 6. Payment Integration (Stripe)
|
||
|
||
### 6.1 Stripe Integration Options
|
||
|
||
#### Option 1: Direct Stripe Integration
|
||
- Simple credit purchases
|
||
- Single merchant (Mana Core)
|
||
- Best for: Centralized credit system
|
||
|
||
#### Option 2: Stripe Connect
|
||
- Marketplace model
|
||
- Multiple payees (if apps pay different parties)
|
||
- Best for: Revenue sharing, marketplace features
|
||
|
||
**Recommendation:** Start with Option 1 (direct), upgrade to Connect if needed.
|
||
|
||
---
|
||
|
||
### 6.2 Stripe Setup for Credit Purchases
|
||
|
||
```typescript
|
||
import Stripe from 'stripe';
|
||
|
||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||
apiVersion: '2024-11-20.acacia',
|
||
});
|
||
|
||
// Create payment intent
|
||
async function createCreditPurchaseIntent(
|
||
userId: string,
|
||
amount: number, // Amount in USD
|
||
quantity: number // Number of credits
|
||
) {
|
||
// Generate idempotency key
|
||
const idempotencyKey = `purchase-${userId}-${Date.now()}`;
|
||
|
||
// Create payment intent
|
||
const paymentIntent = await stripe.paymentIntents.create({
|
||
amount: Math.round(amount * 100), // Stripe uses cents
|
||
currency: 'usd',
|
||
automatic_payment_methods: {
|
||
enabled: true, // Enable all payment methods
|
||
},
|
||
metadata: {
|
||
user_id: userId,
|
||
credit_quantity: quantity,
|
||
idempotency_key: idempotencyKey,
|
||
},
|
||
}, {
|
||
idempotencyKey, // Stripe-level idempotency
|
||
});
|
||
|
||
// Store pending transaction
|
||
await db.query(
|
||
`INSERT INTO transactions (
|
||
user_id, idempotency_key, transaction_type, amount,
|
||
status, metadata
|
||
) VALUES ($1, $2, 'credit_purchase', $3, 'pending', $4)`,
|
||
[
|
||
userId,
|
||
idempotencyKey,
|
||
quantity,
|
||
JSON.stringify({
|
||
stripe_payment_intent_id: paymentIntent.id,
|
||
amount_usd: amount
|
||
})
|
||
]
|
||
);
|
||
|
||
return {
|
||
clientSecret: paymentIntent.client_secret,
|
||
idempotencyKey,
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6.3 Webhook Handling
|
||
|
||
**CRITICAL:** Use webhooks for reliable payment confirmation.
|
||
|
||
```typescript
|
||
import { buffer } from 'micro';
|
||
|
||
export const config = {
|
||
api: {
|
||
bodyParser: false,
|
||
},
|
||
};
|
||
|
||
export default async function handler(req, res) {
|
||
if (req.method !== 'POST') {
|
||
return res.status(405).end();
|
||
}
|
||
|
||
const buf = await buffer(req);
|
||
const sig = req.headers['stripe-signature'];
|
||
|
||
let event;
|
||
|
||
try {
|
||
// Verify webhook signature
|
||
event = stripe.webhooks.constructEvent(
|
||
buf,
|
||
sig,
|
||
process.env.STRIPE_WEBHOOK_SECRET
|
||
);
|
||
} catch (err) {
|
||
console.error('Webhook signature verification failed:', err.message);
|
||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||
}
|
||
|
||
// Handle the event
|
||
switch (event.type) {
|
||
case 'payment_intent.succeeded':
|
||
await handlePaymentSuccess(event.data.object);
|
||
break;
|
||
|
||
case 'payment_intent.payment_failed':
|
||
await handlePaymentFailure(event.data.object);
|
||
break;
|
||
|
||
case 'charge.refunded':
|
||
await handleRefund(event.data.object);
|
||
break;
|
||
|
||
default:
|
||
console.log(`Unhandled event type: ${event.type}`);
|
||
}
|
||
|
||
res.json({ received: true });
|
||
}
|
||
|
||
async function handlePaymentSuccess(paymentIntent) {
|
||
const { user_id, credit_quantity, idempotency_key } = paymentIntent.metadata;
|
||
|
||
await db.query('BEGIN');
|
||
|
||
try {
|
||
// 1. Update transaction status
|
||
await db.query(
|
||
`UPDATE transactions
|
||
SET status = 'completed', completed_at = NOW()
|
||
WHERE idempotency_key = $1`,
|
||
[idempotency_key]
|
||
);
|
||
|
||
// 2. Credit user account
|
||
await creditUserAccount(
|
||
user_id,
|
||
parseFloat(credit_quantity),
|
||
idempotency_key
|
||
);
|
||
|
||
// 3. Send confirmation email
|
||
await sendPurchaseConfirmationEmail(user_id, credit_quantity);
|
||
|
||
await db.query('COMMIT');
|
||
|
||
} catch (error) {
|
||
await db.query('ROLLBACK');
|
||
console.error('Error processing payment success:', error);
|
||
// Alert team for manual intervention
|
||
await alertPaymentProcessingError(paymentIntent.id, error);
|
||
}
|
||
}
|
||
|
||
async function handlePaymentFailure(paymentIntent) {
|
||
const { idempotency_key } = paymentIntent.metadata;
|
||
|
||
await db.query(
|
||
`UPDATE transactions
|
||
SET status = 'failed',
|
||
metadata = metadata || $1
|
||
WHERE idempotency_key = $2`,
|
||
[
|
||
JSON.stringify({
|
||
failure_reason: paymentIntent.last_payment_error?.message
|
||
}),
|
||
idempotency_key
|
||
]
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6.4 Dynamic Pricing & Credit Packages
|
||
|
||
```typescript
|
||
// Define credit packages
|
||
const creditPackages = [
|
||
{ id: 'starter', credits: 100, price: 9.99, savings: 0 },
|
||
{ id: 'plus', credits: 500, price: 39.99, savings: 20 },
|
||
{ id: 'pro', credits: 1000, price: 69.99, savings: 30 },
|
||
{ id: 'enterprise', credits: 5000, price: 299.99, savings: 40 },
|
||
];
|
||
|
||
// Create Stripe product and prices (one-time setup)
|
||
async function setupStripeProducts() {
|
||
for (const pkg of creditPackages) {
|
||
const product = await stripe.products.create({
|
||
name: `${pkg.credits} Credits`,
|
||
description: `Purchase ${pkg.credits} credits${pkg.savings ? ` (${pkg.savings}% savings)` : ''}`,
|
||
metadata: {
|
||
credit_quantity: pkg.credits,
|
||
package_id: pkg.id,
|
||
},
|
||
});
|
||
|
||
const price = await stripe.prices.create({
|
||
product: product.id,
|
||
unit_amount: Math.round(pkg.price * 100),
|
||
currency: 'usd',
|
||
});
|
||
|
||
console.log(`Created package ${pkg.id}: ${price.id}`);
|
||
}
|
||
}
|
||
|
||
// Purchase specific package
|
||
async function purchasePackage(userId: string, packageId: string) {
|
||
const pkg = creditPackages.find(p => p.id === packageId);
|
||
if (!pkg) throw new Error('Invalid package');
|
||
|
||
return await createCreditPurchaseIntent(userId, pkg.price, pkg.credits);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6.5 Payment Method Management
|
||
|
||
```typescript
|
||
// Save payment method for future use
|
||
async function savePaymentMethod(userId: string, paymentMethodId: string) {
|
||
// Create or get Stripe customer
|
||
let customer = await getStripeCustomer(userId);
|
||
|
||
if (!customer) {
|
||
const user = await getUser(userId);
|
||
customer = await stripe.customers.create({
|
||
email: user.email,
|
||
metadata: { user_id: userId },
|
||
});
|
||
|
||
await saveStripeCustomerId(userId, customer.id);
|
||
}
|
||
|
||
// Attach payment method to customer
|
||
await stripe.paymentMethods.attach(paymentMethodId, {
|
||
customer: customer.id,
|
||
});
|
||
|
||
// Set as default
|
||
await stripe.customers.update(customer.id, {
|
||
invoice_settings: {
|
||
default_payment_method: paymentMethodId,
|
||
},
|
||
});
|
||
}
|
||
|
||
// Quick purchase with saved payment method
|
||
async function quickPurchase(userId: string, packageId: string) {
|
||
const customer = await getStripeCustomer(userId);
|
||
const pkg = creditPackages.find(p => p.id === packageId);
|
||
|
||
const paymentIntent = await stripe.paymentIntents.create({
|
||
amount: Math.round(pkg.price * 100),
|
||
currency: 'usd',
|
||
customer: customer.id,
|
||
payment_method: customer.invoice_settings.default_payment_method,
|
||
off_session: true,
|
||
confirm: true,
|
||
metadata: {
|
||
user_id: userId,
|
||
credit_quantity: pkg.credits,
|
||
},
|
||
});
|
||
|
||
return paymentIntent;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6.6 Subscription Model (Optional)
|
||
|
||
```typescript
|
||
// Monthly credit subscription
|
||
async function createCreditSubscription(
|
||
userId: string,
|
||
monthlyCredits: number,
|
||
price: number
|
||
) {
|
||
const customer = await getStripeCustomer(userId);
|
||
|
||
// Create subscription
|
||
const subscription = await stripe.subscriptions.create({
|
||
customer: customer.id,
|
||
items: [{
|
||
price: await getOrCreateSubscriptionPrice(monthlyCredits, price),
|
||
}],
|
||
metadata: {
|
||
user_id: userId,
|
||
monthly_credits: monthlyCredits,
|
||
},
|
||
});
|
||
|
||
// Store subscription in database
|
||
await db.query(
|
||
`INSERT INTO subscriptions (
|
||
user_id, stripe_subscription_id, monthly_credits, status
|
||
) VALUES ($1, $2, $3, $4)`,
|
||
[userId, subscription.id, monthlyCredits, subscription.status]
|
||
);
|
||
|
||
return subscription;
|
||
}
|
||
|
||
// Handle subscription renewal webhook
|
||
async function handleSubscriptionRenewal(subscription) {
|
||
const { user_id, monthly_credits } = subscription.metadata;
|
||
|
||
// Grant monthly credits
|
||
await grantMonthlyCredits(user_id, parseInt(monthly_credits));
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6.7 Stripe Best Practices
|
||
|
||
- [ ] Always verify webhook signatures
|
||
- [ ] Use idempotency keys for all operations
|
||
- [ ] Store Stripe customer ID in user table
|
||
- [ ] Handle all relevant webhook events
|
||
- [ ] Implement retry logic for failed webhooks
|
||
- [ ] Never trust client-side payment amounts
|
||
- [ ] Use metadata extensively for context
|
||
- [ ] Test with Stripe test mode thoroughly
|
||
- [ ] Implement proper error handling
|
||
- [ ] Log all payment-related events
|
||
- [ ] Set up Stripe dashboard alerts
|
||
- [ ] Monitor for fraudulent activity
|
||
- [ ] Provide clear refund policy
|
||
- [ ] Support multiple payment methods
|
||
- [ ] Keep Stripe SDK updated
|
||
|
||
---
|
||
|
||
## 7. Multi-App Authentication Patterns
|
||
|
||
### 7.1 Architecture Overview
|
||
|
||
**Centralized Auth Server Pattern:**
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Mana Core Auth Service │
|
||
│ - Issues: manaToken, appToken, refreshToken │
|
||
│ - Single source of truth for users │
|
||
│ - Manages sessions, devices, 2FA │
|
||
│ - Credit system integrated │
|
||
└────────────────┬────────────────────────────────────────────┘
|
||
│
|
||
│ JWT (RS256)
|
||
│
|
||
┌────────────┴────────────┬───────────────┬────────────┐
|
||
│ │ │ │
|
||
┌───▼───────┐ ┌───────▼────┐ ┌────▼─────┐ ┌──▼──────┐
|
||
│Maerchen- │ │ Memoro │ │ Picture │ │ Chat │
|
||
│ zauber │ │ │ │ │ │ │
|
||
└───────────┘ └────────────┘ └──────────┘ └─────────┘
|
||
- Validates JWT - Validates JWT - Validates - Validates
|
||
- Checks app_id - RLS policies - RLS - RLS
|
||
- Uses credits - Uses credits - Credits - Credits
|
||
```
|
||
|
||
---
|
||
|
||
### 7.2 Token Types in Mana Ecosystem
|
||
|
||
#### 1. manaToken
|
||
- **Purpose:** Universal authentication across all Mana apps
|
||
- **Payload:**
|
||
```typescript
|
||
interface ManaToken {
|
||
sub: string; // user_id
|
||
iss: 'manacore-auth';
|
||
aud: ['manacore', 'maerchenzauber', 'memoro', 'picture', 'chat'];
|
||
exp: number; // 15 minutes
|
||
iat: number;
|
||
role: 'user' | 'admin';
|
||
credits: number; // Current credit balance
|
||
}
|
||
```
|
||
|
||
#### 2. appToken (Supabase-compatible)
|
||
- **Purpose:** App-specific token for Supabase RLS
|
||
- **Payload:**
|
||
```typescript
|
||
interface AppToken {
|
||
sub: string; // user_id
|
||
iss: 'manacore-auth';
|
||
aud: 'maerchenzauber'; // Single app
|
||
app_id: 'maerchenzauber';
|
||
exp: number;
|
||
iat: number;
|
||
role: 'authenticated';
|
||
// App-specific claims
|
||
tenant_id?: string;
|
||
}
|
||
```
|
||
|
||
#### 3. refreshToken
|
||
- **Purpose:** Long-lived token for renewing access
|
||
- **Storage:** Database, single-use with rotation
|
||
- **No payload:** Opaque token looked up in database
|
||
|
||
---
|
||
|
||
### 7.3 Authentication Flow
|
||
|
||
```typescript
|
||
// 1. User login
|
||
async function login(email: string, password: string) {
|
||
// Validate credentials
|
||
const user = await validateCredentials(email, password);
|
||
|
||
// Generate token trio
|
||
const manaToken = generateManaToken(user);
|
||
const refreshToken = generateRefreshToken();
|
||
|
||
// Store session
|
||
await db.query(
|
||
`INSERT INTO sessions (
|
||
user_id, refresh_token, device_info, ip_address, expires_at
|
||
) VALUES ($1, $2, $3, $4, NOW() + INTERVAL '7 days')`,
|
||
[user.id, refreshToken, deviceInfo, ipAddress]
|
||
);
|
||
|
||
return {
|
||
manaToken,
|
||
refreshToken,
|
||
user: {
|
||
id: user.id,
|
||
email: user.email,
|
||
credits: user.credits,
|
||
},
|
||
};
|
||
}
|
||
|
||
// 2. App-specific token request
|
||
async function getAppToken(manaToken: string, appId: string) {
|
||
// Verify mana token
|
||
const payload = jwt.verify(manaToken, publicKey);
|
||
|
||
// Check app access
|
||
const hasAccess = await checkAppAccess(payload.sub, appId);
|
||
if (!hasAccess) throw new Error('No access to app');
|
||
|
||
// Generate app-specific token
|
||
const appToken = jwt.sign(
|
||
{
|
||
sub: payload.sub,
|
||
iss: 'manacore-auth',
|
||
aud: appId,
|
||
app_id: appId,
|
||
role: 'authenticated',
|
||
},
|
||
privateKey,
|
||
{ algorithm: 'RS256', expiresIn: '15m' }
|
||
);
|
||
|
||
return appToken;
|
||
}
|
||
|
||
// 3. Token refresh
|
||
async function refreshTokens(refreshToken: string) {
|
||
// Validate refresh token
|
||
const session = await db.query(
|
||
'SELECT * FROM sessions WHERE refresh_token = $1 AND expires_at > NOW()',
|
||
[refreshToken]
|
||
);
|
||
|
||
if (!session.rows.length) {
|
||
throw new Error('Invalid refresh token');
|
||
}
|
||
|
||
// Check for reuse (security)
|
||
if (session.rows[0].last_used_at &&
|
||
Date.now() - session.rows[0].last_used_at.getTime() < 5000) {
|
||
// Token reuse detected - revoke all sessions
|
||
await revokeAllUserSessions(session.rows[0].user_id);
|
||
throw new Error('Token reuse detected');
|
||
}
|
||
|
||
// Generate new tokens
|
||
const user = await getUser(session.rows[0].user_id);
|
||
const newManaToken = generateManaToken(user);
|
||
const newRefreshToken = generateRefreshToken();
|
||
|
||
// Update session (token rotation)
|
||
await db.query(
|
||
`UPDATE sessions
|
||
SET refresh_token = $1, last_used_at = NOW()
|
||
WHERE id = $2`,
|
||
[newRefreshToken, session.rows[0].id]
|
||
);
|
||
|
||
return {
|
||
manaToken: newManaToken,
|
||
refreshToken: newRefreshToken,
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7.4 Shared Auth Service (@manacore/shared-auth)
|
||
|
||
```typescript
|
||
// packages/shared-auth/src/auth-service.ts
|
||
|
||
export interface AuthServiceConfig {
|
||
authUrl: string; // Mana Core auth API
|
||
appId: string; // App identifier
|
||
storage: AuthStorage; // Platform-specific storage
|
||
}
|
||
|
||
export class AuthService {
|
||
private config: AuthServiceConfig;
|
||
private manaToken: string | null = null;
|
||
private appToken: string | null = null;
|
||
private refreshToken: string | null = null;
|
||
|
||
constructor(config: AuthServiceConfig) {
|
||
this.config = config;
|
||
this.loadTokens();
|
||
}
|
||
|
||
// Login
|
||
async login(email: string, password: string) {
|
||
const response = await fetch(`${this.config.authUrl}/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email, password }),
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
await this.setTokens(data.manaToken, data.refreshToken);
|
||
await this.getAppToken();
|
||
|
||
return data.user;
|
||
}
|
||
|
||
// Get app-specific token
|
||
async getAppToken() {
|
||
if (!this.manaToken) throw new Error('Not authenticated');
|
||
|
||
const response = await fetch(`${this.config.authUrl}/auth/app-token`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${this.manaToken}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ appId: this.config.appId }),
|
||
});
|
||
|
||
const { appToken } = await response.json();
|
||
this.appToken = appToken;
|
||
await this.config.storage.setItem('appToken', appToken);
|
||
|
||
return appToken;
|
||
}
|
||
|
||
// Refresh tokens
|
||
async refresh() {
|
||
if (!this.refreshToken) throw new Error('No refresh token');
|
||
|
||
const response = await fetch(`${this.config.authUrl}/auth/refresh`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ refreshToken: this.refreshToken }),
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
await this.setTokens(data.manaToken, data.refreshToken);
|
||
await this.getAppToken();
|
||
|
||
return data;
|
||
}
|
||
|
||
// Auto-refresh middleware
|
||
async getValidAppToken() {
|
||
// Check if app token expired
|
||
if (this.appToken && this.isTokenExpired(this.appToken)) {
|
||
// Try refreshing
|
||
await this.refresh();
|
||
}
|
||
|
||
return this.appToken;
|
||
}
|
||
|
||
private async setTokens(manaToken: string, refreshToken: string) {
|
||
this.manaToken = manaToken;
|
||
this.refreshToken = refreshToken;
|
||
|
||
await this.config.storage.setItem('manaToken', manaToken);
|
||
await this.config.storage.setItem('refreshToken', refreshToken);
|
||
}
|
||
|
||
private isTokenExpired(token: string): boolean {
|
||
const decoded = jwt.decode(token) as any;
|
||
return decoded.exp * 1000 < Date.now();
|
||
}
|
||
|
||
private async loadTokens() {
|
||
this.manaToken = await this.config.storage.getItem('manaToken');
|
||
this.appToken = await this.config.storage.getItem('appToken');
|
||
this.refreshToken = await this.config.storage.getItem('refreshToken');
|
||
}
|
||
}
|
||
|
||
// Platform-specific storage implementations
|
||
export interface AuthStorage {
|
||
getItem(key: string): Promise<string | null>;
|
||
setItem(key: string, value: string): Promise<void>;
|
||
removeItem(key: string): Promise<void>;
|
||
}
|
||
|
||
// Expo mobile
|
||
export class ExpoSecureStorage implements AuthStorage {
|
||
async getItem(key: string) {
|
||
return await SecureStore.getItemAsync(key);
|
||
}
|
||
async setItem(key: string, value: string) {
|
||
await SecureStore.setItemAsync(key, value);
|
||
}
|
||
async removeItem(key: string) {
|
||
await SecureStore.deleteItemAsync(key);
|
||
}
|
||
}
|
||
|
||
// Web (httpOnly cookies)
|
||
export class BrowserStorage implements AuthStorage {
|
||
// Cookies managed server-side
|
||
async getItem(key: string) {
|
||
// Read from memory or fetch from /auth/session
|
||
return null;
|
||
}
|
||
async setItem(key: string, value: string) {
|
||
// Cookies set by server
|
||
}
|
||
async removeItem(key: string) {
|
||
// Call logout endpoint
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7.5 Usage in Apps
|
||
|
||
#### Expo Mobile App
|
||
```typescript
|
||
// apps/memoro/mobile/App.tsx
|
||
|
||
import { createAuthService } from '@manacore/shared-auth';
|
||
import { ExpoSecureStorage } from '@manacore/shared-auth/storage';
|
||
|
||
const authService = createAuthService({
|
||
authUrl: process.env.EXPO_PUBLIC_MIDDLEWARE_API_URL,
|
||
appId: 'memoro',
|
||
storage: new ExpoSecureStorage(),
|
||
});
|
||
|
||
// Login screen
|
||
const handleLogin = async (email: string, password: string) => {
|
||
try {
|
||
const user = await authService.login(email, password);
|
||
console.log('Logged in:', user);
|
||
} catch (error) {
|
||
console.error('Login failed:', error);
|
||
}
|
||
};
|
||
|
||
// API calls with auto-refresh
|
||
const fetchMemos = async () => {
|
||
const token = await authService.getValidAppToken();
|
||
|
||
const response = await fetch(`${API_URL}/memos`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
return response.json();
|
||
};
|
||
```
|
||
|
||
#### SvelteKit Web App
|
||
```typescript
|
||
// apps/memoro/web/src/hooks.server.ts
|
||
|
||
import { createAuthService } from '@manacore/shared-auth';
|
||
import { SvelteKitStorage } from '@manacore/shared-auth/storage';
|
||
|
||
export async function handle({ event, resolve }) {
|
||
const authService = createAuthService({
|
||
authUrl: import.meta.env.VITE_AUTH_API_URL,
|
||
appId: 'memoro',
|
||
storage: new SvelteKitStorage(event.cookies),
|
||
});
|
||
|
||
// Make auth service available in routes
|
||
event.locals.auth = authService;
|
||
|
||
// Get user if authenticated
|
||
try {
|
||
event.locals.user = await authService.getCurrentUser();
|
||
} catch (error) {
|
||
event.locals.user = null;
|
||
}
|
||
|
||
return resolve(event);
|
||
}
|
||
```
|
||
|
||
#### NestJS Backend
|
||
```typescript
|
||
// apps/memoro/backend/src/auth/auth.guard.ts
|
||
|
||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||
import * as jwt from 'jsonwebtoken';
|
||
import { readFileSync } from 'fs';
|
||
|
||
@Injectable()
|
||
export class JwtAuthGuard implements CanActivate {
|
||
private publicKey = readFileSync('public.key');
|
||
|
||
async canActivate(context: ExecutionContext): boolean {
|
||
const request = context.switchToHttp().getRequest();
|
||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (!token) return false;
|
||
|
||
try {
|
||
// Verify JWT
|
||
const payload = jwt.verify(token, this.publicKey, {
|
||
algorithms: ['RS256'],
|
||
issuer: 'manacore-auth',
|
||
audience: 'memoro',
|
||
});
|
||
|
||
// Set user context
|
||
request.user = payload;
|
||
|
||
// Set database context for RLS
|
||
await this.dataSource.query(
|
||
'SET LOCAL app.current_user_id = $1',
|
||
[payload.sub]
|
||
);
|
||
|
||
return true;
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7.6 Cross-App SSO
|
||
|
||
Users authenticated in one app can seamlessly access others:
|
||
|
||
```typescript
|
||
// User clicks "Open in Memoro" from Maerchenzauber
|
||
|
||
// 1. Maerchenzauber has manaToken
|
||
const manaToken = await authService.getManaToken();
|
||
|
||
// 2. Deep link with token
|
||
const memoroUrl = `memoro://auth?token=${encodeURIComponent(manaToken)}`;
|
||
Linking.openURL(memoroUrl);
|
||
|
||
// 3. Memoro receives token
|
||
export default function App() {
|
||
useEffect(() => {
|
||
Linking.addEventListener('url', async (event) => {
|
||
const url = new URL(event.url);
|
||
if (url.pathname === '/auth') {
|
||
const token = url.searchParams.get('token');
|
||
await authService.setManaToken(token);
|
||
await authService.getAppToken(); // Get Memoro-specific token
|
||
// User now authenticated in Memoro
|
||
}
|
||
});
|
||
}, []);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7.7 Multi-App Patterns Summary
|
||
|
||
**Benefits of Centralized Auth:**
|
||
- ✅ Single source of truth for users
|
||
- ✅ Unified credit system
|
||
- ✅ Seamless cross-app SSO
|
||
- ✅ Consistent security policies
|
||
- ✅ Easier compliance & auditing
|
||
- ✅ Shared auth logic via @manacore/shared-auth
|
||
|
||
**Implementation Checklist:**
|
||
- [ ] Set up Mana Core auth service (NestJS)
|
||
- [ ] Generate RS256 key pair
|
||
- [ ] Create @manacore/shared-auth package
|
||
- [ ] Implement login/refresh endpoints
|
||
- [ ] Build app-token generation endpoint
|
||
- [ ] Create session management system
|
||
- [ ] Integrate with PostgreSQL RLS
|
||
- [ ] Add device tracking
|
||
- [ ] Implement 2FA
|
||
- [ ] Build admin dashboard for user management
|
||
- [ ] Set up monitoring and alerts
|
||
|
||
---
|
||
|
||
## 8. Technology Recommendation Matrix
|
||
|
||
### 8.1 Final Recommendations
|
||
|
||
| Component | Recommended Technology | Alternative | Rationale |
|
||
|-----------|------------------------|-------------|-----------|
|
||
| **Auth Library** | **Better Auth** | Auth.js | Modern, comprehensive, TypeScript-first, no vendor lock-in |
|
||
| **Database** | **PostgreSQL 16+** | - | Industry standard, RLS support, ACID compliance |
|
||
| **ORM** | **Drizzle** | Prisma | Better performance, Better Auth integration, type-safe |
|
||
| **JWT Algorithm** | **RS256** | - | Asymmetric keys for microservices |
|
||
| **Token Storage (Web)** | **httpOnly cookies** | - | XSS protection |
|
||
| **Token Storage (Mobile)** | **Expo SecureStore** | - | Encrypted storage |
|
||
| **Payment Gateway** | **Stripe** | - | Best-in-class, comprehensive features |
|
||
| **Session Management** | **Database + Redis** | DB-only | Redis for blacklist, DB for sessions |
|
||
| **Credit Ledger** | **PostgreSQL** | - | ACID transactions essential |
|
||
|
||
---
|
||
|
||
### 8.2 Pros & Cons Summary
|
||
|
||
#### Better Auth
|
||
**Pros:**
|
||
- ✅ FREE and open-source
|
||
- ✅ Comprehensive features built-in
|
||
- ✅ TypeScript-first with auto-generation
|
||
- ✅ Framework-agnostic
|
||
- ✅ Perfect for monorepos
|
||
- ✅ Active development (YC-backed)
|
||
|
||
**Cons:**
|
||
- ⚠️ New (2024) - less proven
|
||
- ⚠️ Smaller community
|
||
- ⚠️ Documentation still growing
|
||
|
||
#### PostgreSQL + RLS
|
||
**Pros:**
|
||
- ✅ Battle-tested and reliable
|
||
- ✅ RLS provides defense in depth
|
||
- ✅ ACID transactions
|
||
- ✅ Excellent performance
|
||
- ✅ Rich ecosystem
|
||
|
||
**Cons:**
|
||
- ⚠️ Requires expertise to configure securely
|
||
- ⚠️ RLS policies can be complex
|
||
|
||
#### Stripe
|
||
**Pros:**
|
||
- ✅ Comprehensive payment methods
|
||
- ✅ Excellent documentation
|
||
- ✅ Reliable webhooks
|
||
- ✅ Global reach (47+ countries)
|
||
- ✅ Strong fraud prevention
|
||
|
||
**Cons:**
|
||
- ⚠️ 2.9% + $0.30 per transaction
|
||
- ⚠️ Pricing can add up at scale
|
||
|
||
---
|
||
|
||
## 9. Implementation Roadmap
|
||
|
||
### Phase 1: Foundation (Weeks 1-2)
|
||
- [ ] Set up Better Auth with PostgreSQL
|
||
- [ ] Generate RS256 key pair
|
||
- [ ] Create basic auth API (login, register, refresh)
|
||
- [ ] Implement JWT validation middleware
|
||
- [ ] Set up user table with Better Auth schema
|
||
|
||
### Phase 2: Multi-App Integration (Weeks 3-4)
|
||
- [ ] Create @manacore/shared-auth package
|
||
- [ ] Implement app-token generation
|
||
- [ ] Set up session management
|
||
- [ ] Add device tracking
|
||
- [ ] Configure RLS policies for each app
|
||
|
||
### Phase 3: Credit System (Weeks 5-6)
|
||
- [ ] Create credit ledger schema
|
||
- [ ] Implement double-entry bookkeeping
|
||
- [ ] Add idempotency handling
|
||
- [ ] Build credit purchase API
|
||
- [ ] Create credit usage API
|
||
|
||
### Phase 4: Payment Integration (Weeks 7-8)
|
||
- [ ] Set up Stripe account
|
||
- [ ] Implement payment intent creation
|
||
- [ ] Build webhook handlers
|
||
- [ ] Add payment method management
|
||
- [ ] Create credit packages
|
||
|
||
### Phase 5: Advanced Features (Weeks 9-12)
|
||
- [ ] Add 2FA with Better Auth
|
||
- [ ] Implement multi-session management
|
||
- [ ] Build organization/multi-tenancy support
|
||
- [ ] Add OAuth providers
|
||
- [ ] Create admin dashboard
|
||
|
||
### Phase 6: Production Readiness (Weeks 13-14)
|
||
- [ ] Security audit
|
||
- [ ] Performance testing
|
||
- [ ] Set up monitoring and alerts
|
||
- [ ] Write comprehensive tests
|
||
- [ ] Documentation
|
||
- [ ] Deploy to production
|
||
|
||
---
|
||
|
||
## 10. Security Checklist
|
||
|
||
### Authentication
|
||
- [ ] Use Better Auth or battle-tested solution
|
||
- [ ] Implement 2FA for admin accounts
|
||
- [ ] Use SCRAM-SHA-256 for database auth
|
||
- [ ] Rate limit authentication endpoints
|
||
- [ ] Monitor failed login attempts
|
||
- [ ] Implement account lockout after failed attempts
|
||
|
||
### JWT Security
|
||
- [ ] Use RS256 algorithm
|
||
- [ ] Set access token expiration to 15-30 minutes
|
||
- [ ] Implement refresh token rotation
|
||
- [ ] Validate all JWT claims (iss, aud, exp, iat)
|
||
- [ ] Store tokens in httpOnly cookies (web) or secure storage (mobile)
|
||
- [ ] Transmit only over HTTPS
|
||
- [ ] Implement token blacklist for revocation
|
||
|
||
### Database Security
|
||
- [ ] Enable PostgreSQL RLS on all multi-tenant tables
|
||
- [ ] Use prepared statements (prevent SQL injection)
|
||
- [ ] Apply principle of least privilege to database roles
|
||
- [ ] Enable SSL/TLS for database connections
|
||
- [ ] Regular database backups with encryption
|
||
- [ ] Audit logging enabled
|
||
- [ ] Regular security updates
|
||
|
||
### Payment Security
|
||
- [ ] Verify Stripe webhook signatures
|
||
- [ ] Use idempotency keys for all transactions
|
||
- [ ] Never trust client-side amounts
|
||
- [ ] Store sensitive data encrypted
|
||
- [ ] PCI compliance measures
|
||
- [ ] Fraud detection monitoring
|
||
- [ ] Regular reconciliation
|
||
|
||
### General Security
|
||
- [ ] All endpoints behind HTTPS
|
||
- [ ] Input validation and sanitization
|
||
- [ ] Output encoding to prevent XSS
|
||
- [ ] CSRF protection
|
||
- [ ] Security headers (CSP, HSTS, etc.)
|
||
- [ ] Regular dependency updates
|
||
- [ ] Security penetration testing
|
||
- [ ] Incident response plan
|
||
|
||
---
|
||
|
||
## 11. Monitoring & Observability
|
||
|
||
### Key Metrics to Track
|
||
|
||
#### Authentication Metrics
|
||
- Login success/failure rates
|
||
- Token refresh rates
|
||
- Session duration distribution
|
||
- Failed authentication attempts by IP
|
||
- 2FA adoption rate
|
||
- Active sessions per user
|
||
|
||
#### Payment Metrics
|
||
- Credit purchase volume
|
||
- Payment success/failure rates
|
||
- Refund rates
|
||
- Average transaction value
|
||
- Revenue by credit package
|
||
- Payment method distribution
|
||
|
||
#### Credit System Metrics
|
||
- Credit balance distribution
|
||
- Credit consumption rates by app
|
||
- Low balance alerts
|
||
- Expired credits
|
||
- Transaction volume
|
||
- Balance consistency checks
|
||
|
||
#### Security Metrics
|
||
- Suspicious login attempts
|
||
- Token reuse detection
|
||
- Failed authorization attempts
|
||
- RLS policy violations (shouldn't happen if configured correctly)
|
||
- Rate limit hits
|
||
|
||
### Alerting Rules
|
||
```typescript
|
||
// Example alert configurations
|
||
const alerts = [
|
||
{
|
||
name: 'High Failed Login Rate',
|
||
condition: 'failed_logins > 100 per 5 minutes',
|
||
severity: 'high',
|
||
action: 'notify_security_team',
|
||
},
|
||
{
|
||
name: 'Payment Webhook Failure',
|
||
condition: 'webhook_failures > 5 per 10 minutes',
|
||
severity: 'critical',
|
||
action: 'page_on_call',
|
||
},
|
||
{
|
||
name: 'Balance Mismatch Detected',
|
||
condition: 'balance_mismatch_count > 0',
|
||
severity: 'critical',
|
||
action: 'notify_finance_team',
|
||
},
|
||
{
|
||
name: 'Token Reuse Detected',
|
||
condition: 'token_reuse_count > 0',
|
||
severity: 'critical',
|
||
action: 'revoke_sessions_and_alert',
|
||
},
|
||
];
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Additional Resources
|
||
|
||
### Documentation
|
||
- [Better Auth Docs](https://www.better-auth.com/docs)
|
||
- [PostgreSQL RLS Guide](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
||
- [Stripe API Reference](https://docs.stripe.com/api)
|
||
- [JWT Best Practices](https://curity.io/resources/learn/jwt-best-practices/)
|
||
- [OAuth 2.0 RFC](https://datatracker.ietf.org/doc/html/rfc6749)
|
||
|
||
### Libraries & Tools
|
||
- [Better Auth](https://github.com/better-auth/better-auth)
|
||
- [Drizzle ORM](https://orm.drizzle.team/)
|
||
- [jose (JWT library)](https://github.com/panva/jose)
|
||
- [Stripe Node SDK](https://github.com/stripe/stripe-node)
|
||
|
||
### Community
|
||
- [Better Auth Discord](https://discord.gg/better-auth)
|
||
- [PostgreSQL Slack](https://postgres-slack.herokuapp.com/)
|
||
- [Stripe Developers](https://support.stripe.com/developers)
|
||
|
||
---
|
||
|
||
## Conclusion
|
||
|
||
This comprehensive research provides a solid foundation for implementing a secure, scalable, and user-friendly central authentication system for the Mana Universe monorepo.
|
||
|
||
**Key Takeaways:**
|
||
|
||
1. **Better Auth** emerges as the best choice for modern TypeScript monorepos, offering comprehensive features, excellent developer experience, and zero vendor lock-in.
|
||
|
||
2. **PostgreSQL with RLS** provides robust multi-tenancy with defense-in-depth security, essential for a multi-app ecosystem.
|
||
|
||
3. **Double-entry ledger pattern** with idempotency ensures financial accuracy and auditability for the credit system.
|
||
|
||
4. **JWT with RS256** and proper token management (short expiration, rotation) provides secure authentication across multiple apps.
|
||
|
||
5. **Stripe integration** offers reliable payment processing with comprehensive features and global reach.
|
||
|
||
6. **Centralized auth service** with app-specific tokens enables seamless SSO across the Mana ecosystem while maintaining app isolation.
|
||
|
||
The recommended architecture balances security, performance, developer experience, and cost-effectiveness, positioning the Mana Universe for scalable growth.
|
||
|
||
---
|
||
|
||
**End of Report**
|
||
|
||
*For questions or clarifications, consult the Queen agent for aggregation with other research streams.*
|