Skip to content

Commit 1d07ae4

Browse files
Copilothotlong
andcommitted
fix: detect serverless runtime in auto mode, skip file persistence with warning
In serverless environments (Vercel, AWS Lambda, Netlify, Azure Functions, Google Cloud Functions, Deno Deploy), auto persistence mode now disables file-system persistence and emits a warning instead of silently choosing an adapter that will lose data. - Add isServerlessEnvironment() detection via well-known env vars - Auto mode: browser → localStorage, serverless → disabled+warn, Node.js → file - Explicit 'file' or custom adapter still works in serverless (user's choice) - Update schema JSDoc to document serverless behavior - Add 4 new persistence tests for serverless scenarios Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 3946c7b commit 1d07ae4

3 files changed

Lines changed: 153 additions & 4 deletions

File tree

packages/plugins/driver-memory/src/memory-driver.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,18 @@ export interface InMemoryDriverConfig {
3333
logger?: Logger;
3434
/**
3535
* Persistence configuration. Defaults to `'auto'`.
36-
* - `'auto'` (default) — Auto-detect environment (browser → localStorage, Node.js → file)
36+
* - `'auto'` (default) — Auto-detect environment (browser → localStorage, Node.js → file, serverless → disabled)
3737
* - `'file'` — File-system persistence with defaults (Node.js only)
3838
* - `'local'` — localStorage persistence with defaults (Browser only)
3939
* - `{ type: 'file', path?: string, autoSaveInterval?: number }` — File-system with options
4040
* - `{ type: 'local', key?: string }` — localStorage with options
4141
* - `{ type: 'auto', path?: string, key?: string, autoSaveInterval?: number }` — Auto-detect with options
4242
* - `{ adapter: PersistenceAdapterInterface }` — Custom adapter
4343
* - `false` — Disable persistence (pure in-memory)
44+
*
45+
* ⚠️ In serverless environments (Vercel, AWS Lambda, Netlify, etc.),
46+
* auto mode disables file persistence to prevent silent data loss.
47+
* Use `persistence: false` or supply a custom adapter for serverless deployments.
4448
*/
4549
persistence?: string | false | {
4650
type?: 'file' | 'local' | 'auto';
@@ -932,10 +936,43 @@ export class InMemoryDriver implements DriverInterface {
932936
return typeof globalThis.localStorage !== 'undefined';
933937
}
934938

939+
/**
940+
* Detect whether the current runtime is a serverless/edge environment.
941+
*
942+
* Checks well-known environment variables set by serverless platforms:
943+
* - `VERCEL` / `VERCEL_ENV` — Vercel Functions / Edge
944+
* - `AWS_LAMBDA_FUNCTION_NAME` — AWS Lambda
945+
* - `NETLIFY` — Netlify Functions
946+
* - `FUNCTIONS_WORKER_RUNTIME` — Azure Functions
947+
* - `K_SERVICE` — Google Cloud Run / Cloud Functions
948+
* - `FUNCTION_TARGET` — Google Cloud Functions (Node.js)
949+
* - `DENO_DEPLOYMENT_ID` — Deno Deploy
950+
*/
951+
private isServerlessEnvironment(): boolean {
952+
if (typeof globalThis.process === 'undefined' || !globalThis.process.env) {
953+
return false;
954+
}
955+
const env = globalThis.process.env;
956+
return !!(
957+
env.VERCEL ||
958+
env.VERCEL_ENV ||
959+
env.AWS_LAMBDA_FUNCTION_NAME ||
960+
env.NETLIFY ||
961+
env.FUNCTIONS_WORKER_RUNTIME ||
962+
env.K_SERVICE ||
963+
env.FUNCTION_TARGET ||
964+
env.DENO_DEPLOYMENT_ID
965+
);
966+
}
967+
935968
/**
936969
* Initialize the persistence adapter based on configuration.
937970
* Defaults to 'auto' when persistence is not specified.
938971
* Use `persistence: false` to explicitly disable persistence.
972+
*
973+
* In serverless environments (Vercel, AWS Lambda, etc.), auto mode disables
974+
* file-system persistence and emits a warning. Use `persistence: false` or
975+
* supply a custom adapter for serverless-safe operation.
939976
*/
940977
private async initPersistence(): Promise<void> {
941978
const persistence = this.config.persistence === undefined ? 'auto' : this.config.persistence;
@@ -947,6 +984,13 @@ export class InMemoryDriver implements DriverInterface {
947984
const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
948985
this.persistenceAdapter = new LocalStoragePersistenceAdapter();
949986
this.logger.debug('Auto-detected browser environment, using localStorage persistence');
987+
} else if (this.isServerlessEnvironment()) {
988+
this.logger.warn(
989+
'Serverless environment detected — file-system persistence is disabled in auto mode. ' +
990+
'Data will NOT be persisted across function invocations. ' +
991+
'Set persistence: false to silence this warning, or provide a custom adapter ' +
992+
'(e.g. Upstash Redis, Vercel KV) via persistence: { adapter: yourAdapter }.'
993+
);
950994
} else {
951995
const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
952996
this.persistenceAdapter = new FileSystemPersistenceAdapter();
@@ -971,6 +1015,13 @@ export class InMemoryDriver implements DriverInterface {
9711015
key: persistence.key,
9721016
});
9731017
this.logger.debug('Auto-detected browser environment, using localStorage persistence');
1018+
} else if (this.isServerlessEnvironment()) {
1019+
this.logger.warn(
1020+
'Serverless environment detected — file-system persistence is disabled in auto mode. ' +
1021+
'Data will NOT be persisted across function invocations. ' +
1022+
'Set persistence: false to silence this warning, or provide a custom adapter ' +
1023+
'(e.g. Upstash Redis, Vercel KV) via persistence: { adapter: yourAdapter }.'
1024+
);
9741025
} else {
9751026
const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
9761027
this.persistenceAdapter = new FileSystemPersistenceAdapter({

packages/plugins/driver-memory/src/persistence/persistence.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,87 @@ describe('InMemoryDriver Persistence', () => {
212212
await driver2.disconnect();
213213
});
214214
});
215+
216+
describe('Serverless Environment Detection', () => {
217+
const serverlessEnvVars = [
218+
'VERCEL',
219+
'VERCEL_ENV',
220+
'AWS_LAMBDA_FUNCTION_NAME',
221+
'NETLIFY',
222+
'FUNCTIONS_WORKER_RUNTIME',
223+
'K_SERVICE',
224+
'FUNCTION_TARGET',
225+
'DENO_DEPLOYMENT_ID',
226+
];
227+
228+
afterEach(() => {
229+
// Clean up all serverless env vars after each test
230+
for (const key of serverlessEnvVars) {
231+
delete process.env[key];
232+
}
233+
});
234+
235+
it('should disable file persistence in auto mode when VERCEL env is set', async () => {
236+
process.env.VERCEL = '1';
237+
const filePath = path.join(TEST_DATA_DIR, 'serverless-test.json');
238+
const driver = new InMemoryDriver({
239+
persistence: { type: 'auto', path: filePath },
240+
});
241+
await driver.connect();
242+
await driver.create('items', { id: '1', name: 'Widget' });
243+
await driver.flush();
244+
await driver.disconnect();
245+
246+
// File should NOT have been created because auto mode skips file persistence in serverless
247+
expect(fs.existsSync(filePath)).toBe(false);
248+
});
249+
250+
it('should disable file persistence in auto shorthand mode when AWS_LAMBDA_FUNCTION_NAME is set', async () => {
251+
process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-function';
252+
const driver = new InMemoryDriver({ persistence: 'auto' });
253+
await driver.connect();
254+
await driver.create('items', { id: '1', name: 'Widget' });
255+
256+
// Should work as pure in-memory without errors
257+
const items = await driver.find('items', { object: 'items' });
258+
expect(items).toHaveLength(1);
259+
260+
await driver.disconnect();
261+
});
262+
263+
it('should still allow explicit file persistence in serverless if user requests it', async () => {
264+
process.env.VERCEL = '1';
265+
const filePath = path.join(TEST_DATA_DIR, 'explicit-file-serverless.json');
266+
const driver = new InMemoryDriver({
267+
persistence: { type: 'file', path: filePath, autoSaveInterval: 100 },
268+
});
269+
await driver.connect();
270+
await driver.create('items', { id: '1', name: 'Widget' });
271+
await driver.flush();
272+
await driver.disconnect();
273+
274+
// Explicit 'file' type should still create the file even in serverless
275+
expect(fs.existsSync(filePath)).toBe(true);
276+
});
277+
278+
it('should still allow custom adapter in serverless', async () => {
279+
process.env.NETLIFY = 'true';
280+
const stored: Record<string, any[]> = {};
281+
const customAdapter = {
282+
load: async () => Object.keys(stored).length > 0 ? { ...stored } : null,
283+
save: async (db: Record<string, any[]>) => {
284+
for (const [k, v] of Object.entries(db)) { stored[k] = [...v]; }
285+
},
286+
flush: async () => {},
287+
};
288+
289+
const driver = new InMemoryDriver({ persistence: { adapter: customAdapter } });
290+
await driver.connect();
291+
await driver.create('items', { id: '1', name: 'Widget' });
292+
await driver.disconnect();
293+
294+
expect(stored.items).toBeDefined();
295+
expect(stored.items).toHaveLength(1);
296+
});
297+
});
215298
});

packages/spec/src/data/driver/memory.zod.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export type PersistenceAdapter = z.infer<typeof PersistenceAdapterSchema>;
4343
* - `file`: Persist to disk file (Node.js only).
4444
* - `local`: Persist to localStorage (Browser only).
4545
* - `auto`: Auto-detect environment and choose the best strategy.
46-
* Uses `localStorage` in browser environments and `file` in Node.js.
46+
* Uses `localStorage` in browser environments, `file` in standard Node.js,
47+
* and disables persistence with a warning in serverless/edge runtimes.
4748
*/
4849
export const PersistenceTypeSchema = z.enum(['file', 'local', 'auto']).describe('Persistence backend type');
4950

@@ -89,7 +90,14 @@ export type CustomPersistenceConfig = z.infer<typeof CustomPersistenceConfigSche
8990
* Auto-detect persistence configuration.
9091
* Automatically selects the best persistence strategy based on the runtime environment:
9192
* - Browser → localStorage persistence
92-
* - Node.js → File-system persistence
93+
* - Serverless (Vercel, AWS Lambda, etc.) → Persistence disabled with warning
94+
* - Node.js (standard) → File-system persistence
95+
*
96+
* **⚠️ Serverless Warning:** In serverless/edge environments the file system is
97+
* ephemeral or read-only. Auto mode detects this and disables file persistence to
98+
* prevent silent data loss. Use `persistence: false` to silence the warning, or
99+
* supply a custom adapter (e.g. Upstash Redis, Vercel KV) via
100+
* `persistence: { adapter: yourAdapter }`.
93101
*
94102
* Optional overrides allow customizing the file path or localStorage key
95103
* used by the auto-detected adapter.
@@ -112,7 +120,7 @@ export type AutoPersistenceConfig = z.infer<typeof AutoPersistenceConfigSchema>;
112120
* Supports shorthand strings and detailed object configs:
113121
* - `'file'` — File-system persistence with defaults (Node.js)
114122
* - `'local'` — localStorage persistence with defaults (Browser)
115-
* - `'auto'` — Auto-detect environment (browser → localStorage, Node.js → file)
123+
* - `'auto'` — Auto-detect environment (browser → localStorage, Node.js → file, serverless → disabled)
116124
* - `{ type: 'file', path?: string }` — File-system with custom path
117125
* - `{ type: 'local', key?: string }` — localStorage with custom key
118126
* - `{ type: 'auto', path?: string, key?: string }` — Auto-detect with overrides
@@ -191,6 +199,11 @@ export const MemoryConfigSchema = z.object({
191199
* - `{ adapter: PersistenceAdapter }`: Custom persistence adapter
192200
* - `false`: Disable persistence (pure in-memory, data lost on disconnect)
193201
*
202+
* **⚠️ Serverless / Edge environments (Vercel, AWS Lambda, Netlify, etc.):**
203+
* Auto mode detects serverless runtimes and disables file persistence to prevent
204+
* silent data loss. Set `persistence: false` to opt-in to pure in-memory mode,
205+
* or supply a custom adapter (e.g. Upstash Redis, Vercel KV) for durable storage.
206+
*
194207
* @example
195208
* // Auto-detect environment (default)
196209
* new InMemoryDriver()
@@ -200,6 +213,8 @@ export const MemoryConfigSchema = z.object({
200213
* new InMemoryDriver({ persistence: 'local' })
201214
* // Pure memory (no persistence)
202215
* new InMemoryDriver({ persistence: false })
216+
* // Custom adapter for serverless
217+
* new InMemoryDriver({ persistence: { adapter: upstashAdapter } })
203218
*/
204219
persistence: MemoryPersistenceConfigSchema.or(z.literal(false)).default('auto').describe('Persistence configuration (defaults to auto-detect)'),
205220

0 commit comments

Comments
 (0)