Architecture¶
Package Dependency Graph¶
@cyber-eco/types (zero runtime deps)
|
+----------------------+
| |
v v
@cyber-eco/firebase @cyber-eco/auth
(peer: firebase) (peer: firebase, react?, jsonwebtoken?)
| |
| +-----------------+
| |
v v
@cyber-eco/services
(NO firebase dependency)
Why this graph matters¶
@cyber-eco/services sits at the top of the dependency chain but has no dependency on @cyber-eco/firebase. It only depends on:
- @cyber-eco/types — for the StorageAdapter interface and data model types
- @cyber-eco/auth — for error classes and the logger utility
The Firebase connection happens at runtime, when the consumer calls createDataLayer({ adapter: new FirebaseStorageAdapter(...) }). This is the architectural foundation for storage agnosticism.
StorageAdapter Pattern¶
The Interface¶
interface StorageAdapter {
// Document CRUD
getDocument<T>(collection: string, id: string): Promise<T | null>;
setDocument<T>(collection: string, id: string, data: T, options?: WriteOptions): Promise<WriteResult>;
updateDocument(collection: string, id: string, data: Record<string, unknown>): Promise<WriteResult>;
deleteDocument(collection: string, id: string): Promise<WriteResult>;
// Queries
query<T>(collection: string, filters: QueryFilter[], options?: QueryOptions): Promise<PaginatedResult<T>>;
// Batch
batchWrite(operations: BatchOperation[]): Promise<BatchResult>;
// Real-time
subscribe<T>(collection: string, id: string, callback: (data: T | null) => void): Unsubscribe;
subscribeToQuery<T>(collection: string, filters: QueryFilter[], callback: (data: T[]) => void): Unsubscribe;
// Utilities
serverTimestamp(): unknown;
generateId(collection: string): string;
}
Why It Exists¶
Every domain service needs to read and write data. Without the adapter, each service would import firebase/firestore directly, creating an unbreakable dependency on Firebase. The adapter inverts this: services depend on an interface, and the concrete implementation is injected at construction time.
How to Implement a New Adapter¶
- Create a new package (e.g.,
@cyber-eco/ipfs) - Implement the
StorageAdapterinterface - Pass it to
createDataLayer({ adapter: new IPFSStorageAdapter(...) }) - All domain services work unchanged
DataLayerService Orchestration¶
The DataLayerService is the central orchestrator. It coordinates five concerns:
Read Flow¶
Request: get(userId, collection, id)
|
+- 1. Permission Check
| +- Call permissionChecker(userId, collection, 'read', id)
| +- If denied -> throw AuthorizationError
|
+- 2. Cache Lookup
| +- key = `${collection}:${id}`
| +- If hit -> return cached value
|
+- 3. Adapter Fetch
| +- adapter.getDocument(collection, id)
|
+- 4. Cache Set
| +- cache.set(key, result, ttl[collection])
|
+- 5. Return result
Write Flow¶
Request: create(userId, collection, data)
|
+- 1. Permission Check
| +- Call permissionChecker(userId, collection, 'write')
| +- If denied -> throw AuthorizationError
|
+- 2. Enrich Data
| +- Add id, createdAt, createdBy
|
+- 3. Adapter Write
| +- adapter.setDocument(collection, id, enrichedData)
|
+- 4. Cache Invalidate
| +- cache.deletePattern(`${collection}:*`)
| +- cache.set(`${collection}:${id}`, enrichedData)
|
+- 5. Sync Broadcast
| +- sync.broadcast({ type: 'created', collection, documentId, data })
|
+- 6. Webhook Emit
| +- webhooks.emit({ type: `${collection}.created`, payload })
|
+- 7. Return id
Subscription Flow¶
Request: subscribe(userId, collection, id, callback)
|
+- 1. Permission Check (once, at subscription time)
|
+- 2. Delegate to adapter.subscribe(collection, id, wrappedCallback)
| +- wrappedCallback updates cache, then calls user's callback
|
+- 3. Return unsubscribe function
CacheService Architecture¶
+----------------------------------+
| CacheService |
| |
| +------------+ +------------+ |
| | L1 Cache | | L2 Cache | |
| | (In-Memory | | (Optional | |
| | LRU) | | Backend) | |
| | 1000 items | | e.g Redis | |
| +------------+ +------------+ |
| |
| TTL Strategy (per collection): |
| users: 5min |
| permissions: 2min |
| notifications: 30sec |
| transactions: 1min |
| groups: 5min |
| default: 3min |
+----------------------------------+
- L1 is always present (built-in
InMemoryLRUCache) - L2 is optional — plugged in via the
CacheBackendinterface - On read: check L1 -> check L2 -> miss. On L2 hit, promote to L1.
- On write: write to both L1 and L2
deletePattern(pattern)invalidates matching keys across all levelsstaleWhileRevalidate: optionally serve stale data while fetching fresh
Permission Model¶
4-Tier Role Hierarchy¶
hasMinimumRole(userRole, requiredRole) compares numeric values.
AppPermission (merged model)¶
interface AppPermission {
appId: string;
roles: AppRole[];
features: string[];
grantedAt: string;
grantedBy: string;
conditions?: PermissionCondition[];
}
Conditional Access¶
interface PermissionCondition {
type: 'time' | 'ip' | 'mfa' | 'custom';
config: Record<string, unknown>;
}
Conditions are evaluated at access time. All conditions must pass for permission to be active.
- time:
{ after: "2025-01-01", before: "2025-12-31" }— permission valid only within date range - ip:
{ allowList: ["192.168.1.0/24"] }— permission valid only from specified networks - mfa:
{ required: true }— permission requires verified MFA session - custom: extensible for future condition types
Factory Bootstrap¶
The createDataLayer() factory solves the circular dependency between DataLayerService and PermissionService:
function createDataLayer(config: DataLayerConfig): CyberEcoDataLayer {
// 1. Create core without permission checking
const core = new DataLayerService(config);
// 2. Create PermissionService that uses core for data access
const permissions = new PermissionService(core);
// 3. Wire permission checking back into core
core.setPermissionChecker(permissions.evaluatePermission.bind(permissions));
// 4. Create remaining domain services
return {
core,
permissions,
sharedData: new SharedDataService(core),
dashboard: new DashboardService(core),
dataExport: new DataExportService(core),
notifications: new NotificationService(core),
};
}
Collection Schema¶
| Collection | Document Type | Owner | Purpose |
|---|---|---|---|
users |
HubUser / SharedUserProfile | Hub | Central user records |
transactions |
Transaction | Hub | Cross-app financial records |
groups |
SharedGroup | Hub | Cross-app group memberships |
notifications |
Notification | Hub | Cross-app notification queue |
permissions |
ResourcePermission | Hub | Fine-grained resource access |
resourcePermissions |
ResourcePermission | Hub | Resource-level perms (compound key) |
permissionLogs |
PermissionChangeLog | Hub | Audit trail |
userPreferences |
UserPreferences | Hub | Currency, locale prefs |
dataSyncStatus |
DataSyncStatus | Hub | Cross-app sync state |
apps |
App | Hub | App registry |
webhookRegistrations |
WebhookRegistration | Hub | External webhook endpoints |
expenses |
Expense | JustSplit | App-specific expenses |
events |
Event | JustSplit | App-specific events |
settlements |
Settlement | JustSplit | App-specific settlements |
friendships |
Friendship | Shared | Social connections |
Future: Transport Adapter Pattern¶
Parallel to StorageAdapter (which abstracts where data is stored), the TransportAdapter abstracts how data moves between devices:
interface TransportAdapter {
start(): Promise<void>;
stop(): Promise<void>;
send(peerId: string, message: NetworkMessage): Promise<void>;
broadcast(message: NetworkMessage): Promise<void>;
onMessage(callback: (peerId: string, message: NetworkMessage) => void): Unsubscribe;
getPeers(): PeerInfo[];
getTransportType(): TransportType;
}
Implementations: InternetTransport (WebRTC/libp2p), BluetoothTransport (BLE mesh), WiFiDirectTransport, LoRaTransport, WiredTransport (LAN). A MultiTransportManager selects the best transport for each message based on size, urgency, and available networks.
See P2P Architecture for full details.
Future: Encrypted Computation Layer¶
An EncryptedStorageAdapter decorator wraps any StorageAdapter to add transparent client-side encryption:
Write: permissions -> encrypt -> adapter write -> cache -> sync -> return
Read: permissions -> cache -> adapter fetch -> decrypt -> cache set -> return
Three computation methods for different privacy/performance tradeoffs: - FHE — Compute on encrypted data (batch analytics, ~1000x slower) - TEE — Hardware enclaves (real-time queries, ~1.2x slower) - MPC — Multi-party computation (group operations, network-bound)
See Encrypted Computation for full details.
Future Package Graph¶
@cyber-eco/types (zero runtime deps)
|
+----------+----------+----------+
| | | |
v v v v
@cyber-eco/ @cyber-eco/ @cyber-eco/ @cyber-eco/
firebase auth p2p crypto
| | | |
+----+-----+----+----+----+-----+
| | |
v v v
@cyber-eco/ @cyber-eco/ @cyber-eco/
services crdt fhe
All future packages depend only on @cyber-eco/types, maintaining the storage-agnostic constraint.
Related Documents¶
- Vision — Why this exists and the long-term roadmap
- Tokenomics — CYE token economics
- P2P Architecture — Multi-network mesh design
- Encrypted Computation — FHE, TEE, MPC integration