@xtr-dev/rondevu-server 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,87 +1,161 @@
1
1
  /**
2
- * Represents a WebRTC signaling session
2
+ * Represents a WebRTC signaling offer with topic-based discovery
3
3
  */
4
- export interface Session {
5
- code: string;
6
- origin: string;
7
- topic: string;
8
- info: string;
9
- offer: string;
10
- answer?: string;
11
- offerCandidates: string[];
12
- answerCandidates: string[];
4
+ export interface Offer {
5
+ id: string;
6
+ peerId: string;
7
+ sdp: string;
8
+ topics: string[];
13
9
  createdAt: number;
14
10
  expiresAt: number;
11
+ lastSeen: number;
12
+ secret?: string;
13
+ answererPeerId?: string;
14
+ answerSdp?: string;
15
+ answeredAt?: number;
16
+ }
17
+
18
+ /**
19
+ * Represents an ICE candidate for WebRTC signaling
20
+ * Stores the complete candidate object as plain JSON (no type enforcement)
21
+ */
22
+ export interface IceCandidate {
23
+ id: number;
24
+ offerId: string;
25
+ peerId: string;
26
+ role: 'offerer' | 'answerer';
27
+ candidate: any; // Full candidate object as JSON - don't enforce structure
28
+ createdAt: number;
15
29
  }
16
30
 
17
31
  /**
18
- * Storage interface for session management
19
- * Implementations can use different backends (SQLite, Redis, Memory, etc.)
32
+ * Represents a topic with active peer count
33
+ */
34
+ export interface TopicInfo {
35
+ topic: string;
36
+ activePeers: number;
37
+ }
38
+
39
+ /**
40
+ * Request to create a new offer
41
+ */
42
+ export interface CreateOfferRequest {
43
+ id?: string;
44
+ peerId: string;
45
+ sdp: string;
46
+ topics: string[];
47
+ expiresAt: number;
48
+ secret?: string;
49
+ }
50
+
51
+ /**
52
+ * Storage interface for offer management with topic-based discovery
53
+ * Implementations can use different backends (SQLite, D1, Memory, etc.)
20
54
  */
21
55
  export interface Storage {
22
56
  /**
23
- * Creates a new session with the given offer
24
- * @param origin The Origin header from the request
25
- * @param topic The topic to post the offer to
26
- * @param info User info string (max 1024 chars)
27
- * @param offer The WebRTC SDP offer message
28
- * @param expiresAt Unix timestamp when the session should expire
29
- * @returns The unique session code
57
+ * Creates one or more offers
58
+ * @param offers Array of offer creation requests
59
+ * @returns Array of created offers with IDs
60
+ */
61
+ createOffers(offers: CreateOfferRequest[]): Promise<Offer[]>;
62
+
63
+ /**
64
+ * Retrieves offers by topic with optional peer ID exclusion
65
+ * @param topic Topic to search for
66
+ * @param excludePeerIds Optional array of peer IDs to exclude
67
+ * @returns Array of offers matching the topic
68
+ */
69
+ getOffersByTopic(topic: string, excludePeerIds?: string[]): Promise<Offer[]>;
70
+
71
+ /**
72
+ * Retrieves all offers from a specific peer
73
+ * @param peerId Peer identifier
74
+ * @returns Array of offers from the peer
30
75
  */
31
- createSession(origin: string, topic: string, info: string, offer: string, expiresAt: number): Promise<string>;
76
+ getOffersByPeerId(peerId: string): Promise<Offer[]>;
32
77
 
33
78
  /**
34
- * Lists all unanswered sessions for a given origin and topic
35
- * @param origin The Origin header from the request
36
- * @param topic The topic to list offers for
37
- * @returns Array of sessions that haven't been answered yet
79
+ * Retrieves a specific offer by ID
80
+ * @param offerId Offer identifier
81
+ * @returns The offer if found, null otherwise
38
82
  */
39
- listSessionsByTopic(origin: string, topic: string): Promise<Session[]>;
83
+ getOfferById(offerId: string): Promise<Offer | null>;
40
84
 
41
85
  /**
42
- * Lists all topics for a given origin with their session counts
43
- * @param origin The Origin header from the request
44
- * @param page Page number (starting from 1)
45
- * @param limit Number of results per page (max 1000)
46
- * @returns Object with topics array and pagination metadata
86
+ * Deletes an offer (with ownership verification)
87
+ * @param offerId Offer identifier
88
+ * @param ownerPeerId Peer ID of the owner (for verification)
89
+ * @returns true if deleted, false if not found or not owned
47
90
  */
48
- listTopics(origin: string, page: number, limit: number): Promise<{
49
- topics: Array<{ topic: string; count: number }>;
50
- pagination: {
51
- page: number;
52
- limit: number;
53
- total: number;
54
- hasMore: boolean;
55
- };
91
+ deleteOffer(offerId: string, ownerPeerId: string): Promise<boolean>;
92
+
93
+ /**
94
+ * Deletes all expired offers
95
+ * @param now Current timestamp
96
+ * @returns Number of offers deleted
97
+ */
98
+ deleteExpiredOffers(now: number): Promise<number>;
99
+
100
+ /**
101
+ * Answers an offer (locks it to the answerer)
102
+ * @param offerId Offer identifier
103
+ * @param answererPeerId Answerer's peer ID
104
+ * @param answerSdp WebRTC answer SDP
105
+ * @param secret Optional secret for protected offers
106
+ * @returns Success status and optional error message
107
+ */
108
+ answerOffer(offerId: string, answererPeerId: string, answerSdp: string, secret?: string): Promise<{
109
+ success: boolean;
110
+ error?: string;
56
111
  }>;
57
112
 
58
113
  /**
59
- * Retrieves a session by its code
60
- * @param code The session code
61
- * @param origin The Origin header from the request (for validation)
62
- * @returns The session if found, null otherwise
114
+ * Retrieves all answered offers for a specific offerer
115
+ * @param offererPeerId Offerer's peer ID
116
+ * @returns Array of answered offers
63
117
  */
64
- getSession(code: string, origin: string): Promise<Session | null>;
118
+ getAnsweredOffers(offererPeerId: string): Promise<Offer[]>;
65
119
 
66
120
  /**
67
- * Updates an existing session with new data
68
- * @param code The session code
69
- * @param origin The Origin header from the request (for validation)
70
- * @param update Partial session data to update
121
+ * Adds ICE candidates for an offer
122
+ * @param offerId Offer identifier
123
+ * @param peerId Peer ID posting the candidates
124
+ * @param role Role of the peer (offerer or answerer)
125
+ * @param candidates Array of candidate objects (stored as plain JSON)
126
+ * @returns Number of candidates added
71
127
  */
72
- updateSession(code: string, origin: string, update: Partial<Session>): Promise<void>;
128
+ addIceCandidates(
129
+ offerId: string,
130
+ peerId: string,
131
+ role: 'offerer' | 'answerer',
132
+ candidates: any[]
133
+ ): Promise<number>;
73
134
 
74
135
  /**
75
- * Deletes a session
76
- * @param code The session code
136
+ * Retrieves ICE candidates for an offer
137
+ * @param offerId Offer identifier
138
+ * @param targetRole Role to retrieve candidates for (offerer or answerer)
139
+ * @param since Optional timestamp - only return candidates after this time
140
+ * @returns Array of ICE candidates
77
141
  */
78
- deleteSession(code: string): Promise<void>;
142
+ getIceCandidates(
143
+ offerId: string,
144
+ targetRole: 'offerer' | 'answerer',
145
+ since?: number
146
+ ): Promise<IceCandidate[]>;
79
147
 
80
148
  /**
81
- * Removes expired sessions
82
- * Should be called periodically to clean up old data
149
+ * Retrieves topics with active peer counts (paginated)
150
+ * @param limit Maximum number of topics to return
151
+ * @param offset Number of topics to skip
152
+ * @param startsWith Optional prefix filter - only return topics starting with this string
153
+ * @returns Object with topics array and total count
83
154
  */
84
- cleanup(): Promise<void>;
155
+ getTopics(limit: number, offset: number, startsWith?: string): Promise<{
156
+ topics: TopicInfo[];
157
+ total: number;
158
+ }>;
85
159
 
86
160
  /**
87
161
  * Closes the storage connection and releases resources
package/src/worker.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  import { createApp } from './app.ts';
2
- import { KVStorage } from './storage/kv.ts';
2
+ import { D1Storage } from './storage/d1.ts';
3
+ import { generateSecretKey } from './crypto.ts';
4
+ import { Config } from './config.ts';
3
5
 
4
6
  /**
5
7
  * Cloudflare Workers environment bindings
6
8
  */
7
9
  export interface Env {
8
- SESSIONS: KVNamespace;
9
- SESSION_TIMEOUT?: string;
10
+ DB: D1Database;
11
+ AUTH_SECRET?: string;
12
+ OFFER_DEFAULT_TTL?: string;
13
+ OFFER_MAX_TTL?: string;
14
+ OFFER_MIN_TTL?: string;
15
+ MAX_OFFERS_PER_REQUEST?: string;
16
+ MAX_TOPICS_PER_OFFER?: string;
10
17
  CORS_ORIGINS?: string;
18
+ VERSION?: string;
11
19
  }
12
20
 
13
21
  /**
@@ -15,25 +23,52 @@ export interface Env {
15
23
  */
16
24
  export default {
17
25
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
18
- // Initialize KV storage
19
- const storage = new KVStorage(env.SESSIONS);
26
+ // Initialize D1 storage
27
+ const storage = new D1Storage(env.DB);
20
28
 
21
- // Parse configuration
22
- const sessionTimeout = env.SESSION_TIMEOUT
23
- ? parseInt(env.SESSION_TIMEOUT, 10)
24
- : 300000; // 5 minutes default
29
+ // Generate or use provided auth secret
30
+ const authSecret = env.AUTH_SECRET || generateSecretKey();
25
31
 
26
- const corsOrigins = env.CORS_ORIGINS
27
- ? env.CORS_ORIGINS.split(',').map(o => o.trim())
28
- : ['*'];
32
+ // Build config from environment
33
+ const config: Config = {
34
+ port: 0, // Not used in Workers
35
+ storageType: 'sqlite', // D1 is SQLite-compatible
36
+ storagePath: '', // Not used with D1
37
+ corsOrigins: env.CORS_ORIGINS
38
+ ? env.CORS_ORIGINS.split(',').map(o => o.trim())
39
+ : ['*'],
40
+ version: env.VERSION || 'unknown',
41
+ authSecret,
42
+ offerDefaultTtl: env.OFFER_DEFAULT_TTL ? parseInt(env.OFFER_DEFAULT_TTL, 10) : 60000,
43
+ offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : 86400000,
44
+ offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : 60000,
45
+ cleanupInterval: 60000, // Not used in Workers (scheduled handler instead)
46
+ maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100,
47
+ maxTopicsPerOffer: env.MAX_TOPICS_PER_OFFER ? parseInt(env.MAX_TOPICS_PER_OFFER, 10) : 50,
48
+ };
29
49
 
30
50
  // Create Hono app
31
- const app = createApp(storage, {
32
- sessionTimeout,
33
- corsOrigins,
34
- });
51
+ const app = createApp(storage, config);
35
52
 
36
53
  // Handle request
37
54
  return app.fetch(request, env, ctx);
38
55
  },
56
+
57
+ /**
58
+ * Scheduled handler for cron triggers
59
+ * Runs periodically to clean up expired offers
60
+ */
61
+ async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
62
+ const storage = new D1Storage(env.DB);
63
+ const now = Date.now();
64
+
65
+ try {
66
+ // Delete expired offers
67
+ const deletedCount = await storage.deleteExpiredOffers(now);
68
+
69
+ console.log(`Cleaned up ${deletedCount} expired offers at ${new Date(now).toISOString()}`);
70
+ } catch (error) {
71
+ console.error('Error cleaning up offers:', error);
72
+ }
73
+ },
39
74
  };
package/wrangler.toml ADDED
@@ -0,0 +1,45 @@
1
+ name = "rondevu"
2
+ main = "src/worker.ts"
3
+ compatibility_date = "2024-01-01"
4
+ compatibility_flags = ["nodejs_compat"]
5
+
6
+ # D1 Database binding
7
+ [[d1_databases]]
8
+ binding = "DB"
9
+ database_name = "rondevu-offers"
10
+ database_id = "b94e3f71-816d-455b-a89d-927fa49532d0"
11
+
12
+ # Environment variables
13
+ [vars]
14
+ OFFER_DEFAULT_TTL = "60000" # Default offer TTL: 1 minute
15
+ OFFER_MAX_TTL = "86400000" # Max offer TTL: 24 hours
16
+ OFFER_MIN_TTL = "60000" # Min offer TTL: 1 minute
17
+ MAX_OFFERS_PER_REQUEST = "100" # Max offers per request
18
+ MAX_TOPICS_PER_OFFER = "50" # Max topics per offer
19
+ CORS_ORIGINS = "*" # Comma-separated list of allowed origins
20
+ VERSION = "0.1.0" # Semantic version
21
+
22
+ # AUTH_SECRET should be set as a secret, not a var
23
+ # Run: npx wrangler secret put AUTH_SECRET
24
+ # Enter a 64-character hex string (32 bytes)
25
+
26
+ # Build configuration
27
+ [build]
28
+ command = ""
29
+
30
+ # For local development:
31
+ # Run: npx wrangler dev
32
+ # The local D1 database will be created automatically
33
+
34
+ # For production deployment:
35
+ # 1. Create D1 database: npx wrangler d1 create rondevu-sessions
36
+ # 2. Update the 'database_id' field above with the returned ID
37
+ # 3. Initialize schema: npx wrangler d1 execute rondevu-sessions --remote --file=./migrations/schema.sql
38
+ # 4. Deploy: npx wrangler deploy
39
+
40
+ [observability]
41
+ [observability.logs]
42
+ enabled = false
43
+ head_sampling_rate = 1
44
+ invocation_logs = true
45
+ persist = true
package/DEPLOYMENT.md DELETED
@@ -1,346 +0,0 @@
1
- # Deployment Guide
2
-
3
- This guide covers deploying Rondevu to various platforms.
4
-
5
- ## Table of Contents
6
-
7
- - [Cloudflare Workers](#cloudflare-workers)
8
- - [Docker](#docker)
9
- - [Node.js](#nodejs)
10
-
11
- ---
12
-
13
- ## Cloudflare Workers
14
-
15
- Deploy to Cloudflare's edge network using Cloudflare Workers and KV storage.
16
-
17
- ### Prerequisites
18
-
19
- ```bash
20
- npm install -g wrangler
21
- ```
22
-
23
- ### Setup
24
-
25
- 1. **Login to Cloudflare**
26
- ```bash
27
- wrangler login
28
- ```
29
-
30
- 2. **Create KV Namespace**
31
- ```bash
32
- # For production
33
- wrangler kv:namespace create SESSIONS
34
-
35
- # This will output something like:
36
- # { binding = "SESSIONS", id = "abc123..." }
37
- ```
38
-
39
- 3. **Update wrangler.toml**
40
-
41
- Edit `wrangler.toml` and replace `YOUR_KV_NAMESPACE_ID` with the ID from step 2:
42
-
43
- ```toml
44
- [[kv_namespaces]]
45
- binding = "SESSIONS"
46
- id = "abc123..." # Your actual KV namespace ID
47
- ```
48
-
49
- 4. **Configure Environment Variables** (Optional)
50
-
51
- Update `wrangler.toml` to customize settings:
52
-
53
- ```toml
54
- [vars]
55
- SESSION_TIMEOUT = "300000" # Session timeout in milliseconds
56
- CORS_ORIGINS = "https://example.com,https://app.example.com"
57
- ```
58
-
59
- ### Local Development
60
-
61
- ```bash
62
- # Run locally with Wrangler
63
- npx wrangler dev
64
-
65
- # The local development server will:
66
- # - Start on http://localhost:8787
67
- # - Use a local KV namespace automatically
68
- # - Hot-reload on file changes
69
- ```
70
-
71
- ### Production Deployment
72
-
73
- ```bash
74
- # Deploy to Cloudflare Workers
75
- npx wrangler deploy
76
-
77
- # This will output your worker URL:
78
- # https://rondevu.YOUR_SUBDOMAIN.workers.dev
79
- ```
80
-
81
- ### Custom Domain (Optional)
82
-
83
- 1. Go to your Cloudflare Workers dashboard
84
- 2. Select your worker
85
- 3. Click "Triggers" → "Add Custom Domain"
86
- 4. Enter your domain (e.g., `api.example.com`)
87
-
88
- ### Monitoring
89
-
90
- View logs and analytics:
91
-
92
- ```bash
93
- # Stream real-time logs
94
- npx wrangler tail
95
-
96
- # View in dashboard
97
- # Visit: https://dash.cloudflare.com → Workers & Pages
98
- ```
99
-
100
- ### Environment Variables
101
-
102
- | Variable | Default | Description |
103
- |----------|---------|-------------|
104
- | `SESSION_TIMEOUT` | `300000` | Session timeout in milliseconds |
105
- | `CORS_ORIGINS` | `*` | Comma-separated allowed origins |
106
-
107
- ### Pricing
108
-
109
- Cloudflare Workers Free Tier includes:
110
- - 100,000 requests/day
111
- - 10ms CPU time per request
112
- - KV: 100,000 reads/day, 1,000 writes/day
113
-
114
- For higher usage, see [Cloudflare Workers pricing](https://workers.cloudflare.com/#plans).
115
-
116
- ### Advantages
117
-
118
- - **Global Edge Network**: Deploy to 300+ locations worldwide
119
- - **Instant Scaling**: Handles traffic spikes automatically
120
- - **Low Latency**: Runs close to your users
121
- - **No Server Management**: Fully serverless
122
- - **Free Tier**: Generous limits for small projects
123
-
124
- ---
125
-
126
- ## Docker
127
-
128
- ### Quick Start
129
-
130
- ```bash
131
- # Build
132
- docker build -t rondevu .
133
-
134
- # Run with in-memory SQLite
135
- docker run -p 3000:3000 -e STORAGE_PATH=:memory: rondevu
136
-
137
- # Run with persistent SQLite
138
- docker run -p 3000:3000 \
139
- -v $(pwd)/data:/app/data \
140
- -e STORAGE_PATH=/app/data/sessions.db \
141
- rondevu
142
- ```
143
-
144
- ### Docker Compose
145
-
146
- Create a `docker-compose.yml`:
147
-
148
- ```yaml
149
- version: '3.8'
150
-
151
- services:
152
- rondevu:
153
- build: .
154
- ports:
155
- - "3000:3000"
156
- environment:
157
- - PORT=3000
158
- - STORAGE_TYPE=sqlite
159
- - STORAGE_PATH=/app/data/sessions.db
160
- - SESSION_TIMEOUT=300000
161
- - CORS_ORIGINS=*
162
- volumes:
163
- - ./data:/app/data
164
- restart: unless-stopped
165
- ```
166
-
167
- Run with:
168
- ```bash
169
- docker-compose up -d
170
- ```
171
-
172
- ### Environment Variables
173
-
174
- | Variable | Default | Description |
175
- |----------|---------|-------------|
176
- | `PORT` | `3000` | Server port |
177
- | `STORAGE_TYPE` | `sqlite` | Storage backend |
178
- | `STORAGE_PATH` | `/app/data/sessions.db` | SQLite database path |
179
- | `SESSION_TIMEOUT` | `300000` | Session timeout in ms |
180
- | `CORS_ORIGINS` | `*` | Allowed CORS origins |
181
-
182
- ---
183
-
184
- ## Node.js
185
-
186
- ### Production Deployment
187
-
188
- 1. **Install Dependencies**
189
- ```bash
190
- npm ci --production
191
- ```
192
-
193
- 2. **Build TypeScript**
194
- ```bash
195
- npm run build
196
- ```
197
-
198
- 3. **Set Environment Variables**
199
- ```bash
200
- export PORT=3000
201
- export STORAGE_TYPE=sqlite
202
- export STORAGE_PATH=./data/sessions.db
203
- export SESSION_TIMEOUT=300000
204
- export CORS_ORIGINS=*
205
- ```
206
-
207
- 4. **Run**
208
- ```bash
209
- npm start
210
- ```
211
-
212
- ### Process Manager (PM2)
213
-
214
- For production, use a process manager like PM2:
215
-
216
- 1. **Install PM2**
217
- ```bash
218
- npm install -g pm2
219
- ```
220
-
221
- 2. **Create ecosystem.config.js**
222
- ```javascript
223
- module.exports = {
224
- apps: [{
225
- name: 'rondevu',
226
- script: './dist/index.js',
227
- instances: 'max',
228
- exec_mode: 'cluster',
229
- env: {
230
- NODE_ENV: 'production',
231
- PORT: 3000,
232
- STORAGE_TYPE: 'sqlite',
233
- STORAGE_PATH: './data/sessions.db',
234
- SESSION_TIMEOUT: 300000,
235
- CORS_ORIGINS: '*'
236
- }
237
- }]
238
- };
239
- ```
240
-
241
- 3. **Start with PM2**
242
- ```bash
243
- pm2 start ecosystem.config.js
244
- pm2 save
245
- pm2 startup
246
- ```
247
-
248
- ### Systemd Service
249
-
250
- Create `/etc/systemd/system/rondevu.service`:
251
-
252
- ```ini
253
- [Unit]
254
- Description=Rondevu Peer Discovery and Signaling Server
255
- After=network.target
256
-
257
- [Service]
258
- Type=simple
259
- User=www-data
260
- WorkingDirectory=/opt/rondevu
261
- ExecStart=/usr/bin/node dist/index.js
262
- Restart=on-failure
263
- Environment=PORT=3000
264
- Environment=STORAGE_TYPE=sqlite
265
- Environment=STORAGE_PATH=/opt/rondevu/data/sessions.db
266
- Environment=SESSION_TIMEOUT=300000
267
- Environment=CORS_ORIGINS=*
268
-
269
- [Install]
270
- WantedBy=multi-user.target
271
- ```
272
-
273
- Enable and start:
274
- ```bash
275
- sudo systemctl enable rondevu
276
- sudo systemctl start rondevu
277
- sudo systemctl status rondevu
278
- ```
279
-
280
- ---
281
-
282
- ## Troubleshooting
283
-
284
- ### Docker
285
-
286
- **Issue: Permission denied on /app/data**
287
- - Ensure volume permissions are correct
288
- - The container runs as user `node` (UID 1000)
289
-
290
- **Issue: Database locked**
291
- - Don't share the same SQLite database file across multiple containers
292
- - Use one instance or implement a different storage backend
293
-
294
- ### Node.js
295
-
296
- **Issue: EADDRINUSE**
297
- - Port is already in use, change `PORT` environment variable
298
-
299
- **Issue: Database is locked**
300
- - Another process is using the database
301
- - Ensure only one instance is running with the same database file
302
-
303
- ---
304
-
305
- ## Performance Tuning
306
-
307
- ### Node.js/Docker
308
-
309
- - Set `SESSION_TIMEOUT` appropriately to balance resource usage
310
- - For high traffic, use `STORAGE_PATH=:memory:` with session replication
311
- - Consider horizontal scaling with a shared database backend
312
-
313
- ---
314
-
315
- ## Security Considerations
316
-
317
- 1. **HTTPS**: Always use HTTPS in production
318
- - Use a reverse proxy (nginx, Caddy) for Node.js deployments
319
- - Docker deployments should be behind a reverse proxy
320
-
321
- 2. **Rate Limiting**: Implement rate limiting at the proxy level
322
-
323
- 3. **CORS**: Configure CORS origins appropriately
324
- - Don't use `*` in production
325
- - Set specific allowed origins: `https://example.com,https://app.example.com`
326
-
327
- 4. **Input Validation**: SDP offers/answers are stored as-is; validate on client side
328
-
329
- 5. **Session Codes**: UUID v4 codes provide strong entropy (2^122 combinations)
330
-
331
- 6. **Origin Isolation**: Sessions are isolated by Origin header to organize topics by domain
332
-
333
- ---
334
-
335
- ## Scaling
336
-
337
- ### Horizontal Scaling
338
-
339
- - **Docker/Node.js**: Use a shared database (not SQLite) for multiple instances
340
- - Implement a Redis or PostgreSQL storage adapter
341
-
342
- ### Vertical Scaling
343
-
344
- - Increase `SESSION_TIMEOUT` or cleanup frequency as needed
345
- - Monitor database size and connection pool
346
- - For Node.js, monitor memory usage and increase if needed