@delta-base/auth
v0.2.7
Published
DeltaBase Auth Service
Keywords
Readme
Auth Service
A clean architecture implementation for user authentication and authorization built with event sourcing and CQRS patterns.
Architecture Overview
This service follows Clean Architecture principles with clear separation of concerns across three main layers:
graph TD
A[HTTP Request] --> B[Route Layer]
B --> C[Handler Layer]
C --> D[Domain Layer]
subgraph "Route Layer - HTTP/Infrastructure Concerns"
B1[Input Validation]
B2[Authentication Check<br/>is user logged in?]
B3[Response Formatting]
B4[HTTP Status Codes]
B5[Error Mapping]
end
subgraph "Handler Layer - Application Concerns"
C1[Authorization Check<br/>can user perform action?]
C2[Permission Validation]
C3[Cross-cutting Concerns]
C4[Orchestration]
end
subgraph "Domain Layer - Business Logic"
D1[Business Rules]
D2[Domain Validation]
D3[State Changes]
D4[Event Generation]
end
B --> B1
B1 --> B2
B2 --> C1
C1 --> C2
C2 --> C4
C4 --> D1
D1 --> D2
D2 --> D3
D3 --> D4
style B fill:#e1f5fe
style C fill:#f3e5f5
style D fill:#e8f5e8Quick Setup
Prerequisites
Start your Deltabase platform (required):
# From project root pnpm --filter @delta-base/core devInstall dependencies:
# From auth package directory pnpm install
Environment Setup
Your environment variables are configured in .dev.vars:
DELTABASE_URL=http://localhost:8787 # Platform API URL
DELTABASE_API_KEY=dev-key # API key for platform access
DELTABASE_EVENT_STORE_ID=auth-service # Event store nameInitial Setup (Idempotent)
Run the setup script to create your event store and subscriptions:
# For development (uses local platform)
pnpm run setup:dev
# For production (uses environment variables)
pnpm run setupThe setup script will:
- ✅ Create the
auth-serviceevent store (idempotent) - ✅ Setup projection subscriptions for user events (idempotent)
- ✅ Validate the configuration
- ✅ Display the event store ID for your
.dev.vars
Example output:
🚀 Starting Auth Service setup
📍 Target: http://localhost:8787
🏪 Setting up event store: auth-service
✅ Event store ready: es_2vbr3EKPNnmBfPJpUi0bl
🔗 Setting up projection subscriptions
✅ Users projection subscription created: auth-users-projection
🔍 Validating setup
✅ Event store accessible (0 streams found)
✅ Event bus accessible (1 subscriptions found)
✨ Auth Service setup completed successfully!
🎉 Setup complete! Next steps:
1. Update your .dev.vars with the event store ID:
DELTABASE_EVENT_STORE_ID=es_2vbr3EKPNnmBfPJpUi0bl
2. Run your auth service to start processing events
3. Check the deltabase studio for real-time monitoringDevelopment Workflow
# 1. Run setup (first time or when adding new projections)
pnpm run setup:dev
# 2. Start development server
pnpm run dev
# 3. Your service is now running with:
# - Event sourcing configured
# - Projections subscribed and processing
# - Real-time monitoring availableProduction Deployment
Set environment variables in your production environment:
DELTABASE_URL=https://your-platform.example.com DELTABASE_API_KEY=your-production-api-key DELTABASE_EVENT_STORE_NAME=auth-service-prodRun setup in production:
pnpm run setupDeploy your service with the returned event store ID.
Layer Responsibilities
| Layer | Authentication | Authorization | Business Rules | HTTP Concerns | |-------|---------------|---------------|----------------|---------------| | Route | ✅ Basic (JWT valid?) | ❌ | ❌ | ✅ | | Handler | ❌ | ✅ Permissions | ❌ | ❌ | | Domain | ❌ | ❌ | ✅ Business Logic | ❌ |
🌐 Route Layer
Purpose: Handle HTTP transport concerns and external interface
Responsibilities:
- Input validation and sanitization
- Basic authentication checks (is user logged in?)
- Response formatting and HTTP status codes
- Error message mapping for clients
- Rate limiting and request throttling
Example:
export async function createUserRoute(
eventStore: EventStore,
actor: Actor,
request: CreateUserRouteRequest
): Promise<CreateUserRouteResponse | CreateUserRouteError> {
// 1. Input validation
if (!request.email || !request.firstName || !request.lastName) {
return { success: false, message: 'Missing required fields' };
}
// 2. Basic authentication check
if (!actor || actor.type === 'public') {
return { success: false, message: 'Authentication required' };
}
// 3. Call handler (authorization happens there)
const result = await createUser(eventStore, actor, request);
// 4. Format response
return {
success: true,
message: 'User created successfully',
data: { id: result.id, user: result.user }
};
}🛡️ Handler Layer
Purpose: Application orchestration and cross-cutting concerns
Responsibilities:
- Permission-based authorization (can user perform this action?)
- Role-based access control
- Cross-cutting concerns (logging, metrics, caching)
- Orchestrating calls to domain and infrastructure services
Example:
export async function createUser(
eventStore: EventStore,
actor: Actor,
request: CreateUserRequest
): Promise<CreateUserResult> {
// Authorization check at application level
requireUserCreationPermission(actor);
const streamId = Users.generateStreamId(request.email);
const command: Users.CreateUser = {
type: 'user.create',
data: request,
metadata: { actor },
};
const result = await handleCommandWithDecider(
eventStore,
streamId,
command,
Users.decider,
);
return {
id: streamId,
user: result.newState,
nextExpectedStreamVersion: result.appendResult.nextExpectedStreamVersion,
};
}🏛️ Domain Layer
Purpose: Pure business logic and domain rules
Responsibilities:
- Business rule validation
- State transitions and invariants
- Event generation
- Domain-specific constraints
Example:
export const createUserHandler = (command: CreateUser, state: UserEntity): UserCreated => {
// Business rule: User must not already exist
if (state.id && state.id !== '') {
throw new IllegalStateError(`user with email ${command.data.email} already exists`);
}
// Generate domain event
return {
type: 'user.created',
data: {
id: generateStreamId(command.data.email),
firstName: command.data.firstName,
lastName: command.data.lastName,
email: command.data.email
},
metadata: { actor: command.metadata.actor }
};
};Authorization Patterns
Simple Permission Check
function requireUserCreationPermission(actor: Actor): void {
switch (actor.type) {
case 'user':
if (!actor.properties.userID || !actor.properties.organizationID) {
throw new Error('User actor must have userID and organizationID');
}
break;
case 'system':
if (!actor.properties.organizationID) {
throw new Error('System actor must have organizationID');
}
break;
case 'public':
throw new Error('Public actors cannot create users');
default:
throw new Error('Invalid actor type');
}
}Advanced Permission Service
export interface PermissionService {
hasPermission(actor: Actor, permission: string, resource?: string): Promise<boolean>;
}
async function requireUserCreationPermissionAsync(
actor: Actor,
permissionService: PermissionService
): Promise<void> {
const hasPermission = await permissionService.hasPermission(
actor,
'users:create',
'organization'
);
if (!hasPermission) {
throw new Error('Permission denied: users:create required');
}
}Project Structure
packages/auth/
├── src/
│ ├── core/
│ │ ├── actor.ts # Actor types and utilities
│ │ ├── context.ts # Context management
│ │ └── users/
│ │ ├── users.ts # User domain logic
│ │ └── users.test.ts # Domain tests
│ ├── functions/
│ │ └── users/
│ │ └── create-user/
│ │ ├── handler.ts # Application service
│ │ └── route.ts # HTTP route handler
│ ├── shared/ # Shared utilities
│ ├── index.ts # Public API
│ └── subject.ts # Subject management
├── package.json
├── tsconfig.json
└── README.mdKey Design Decisions
1. Functional over Object-Oriented
- Why: Simpler, more testable, better composability
- Pattern: Pure functions with dependency injection via parameters
2. No Messages in Business Layer
- Why: Messages are presentation concerns, not business concerns
- Pattern: Handler returns data, route formats messages
3. Authorization in Handler Layer
- Why: Application-level concern, reusable across transports
- Pattern: Permission checks before domain operations
4. Event Sourcing with CQRS
- Why: Audit trail, temporal queries, scalability
- Pattern: Commands → Events → Projections
Usage Examples
Basic User Creation
import { createUser } from '@/functions/users/create-user/handler';
const result = await createUser(eventStore, actor, {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe'
});With Permission Service
import { createUserWithPermissions } from '@/functions/users/create-user/handler';
const result = await createUserWithPermissions(
eventStore,
permissionService,
actor,
request
);HTTP Route
import { createUserRoute } from '@/functions/users/create-user/route';
app.post('/users', async (req, res) => {
const result = await createUserRoute(eventStore, actor, req.body);
if (result.success) {
res.status(201).json(result);
} else {
res.status(400).json(result);
}
});Testing Strategy
Unit Tests
- Domain: Test business rules in isolation
- Handlers: Test authorization and orchestration
- Routes: Test HTTP concerns and error handling
Integration Tests
- Event Store: Test event persistence and replay
- Full Flow: Test complete request/response cycles
Example Test
describe('createUser', () => {
it('should reject public actors', async () => {
const actor: Actor = { type: 'public', properties: {} };
await expect(createUser(eventStore, actor, request))
.rejects.toThrow('Public actors cannot create users');
});
});Best Practices
- Keep handlers thin: Orchestrate, don't implement business logic
- Pure domain functions: No side effects in domain layer
- Explicit dependencies: Pass dependencies as parameters
- Type safety: Use TypeScript strictly for better DX
- Test each layer: Unit test business logic, integration test flows
- Error boundaries: Handle errors at appropriate layers
- Documentation: Document business rules and authorization policies
Contributing
- Follow the layer responsibilities outlined above
- Write tests for new functionality
- Keep functions pure and composable
- Use meaningful error messages
- Document authorization requirements
