@akirilyuk/supabase-in-memory-server
v1.0.1
Published
In-memory HTTP server mimicking Supabase REST (PostgREST) and Auth (GoTrue) for supabase-js: local testing, e2e, anon/service-role keys, optional CLI, Winston logs
Maintainers
Readme
supabase-in-memory-server
npm package supabase-in-memory-server (formerly supabase-jest-mock).
In-memory HTTP server that mimics the parts of Supabase that @supabase/supabase-js talks to: PostgREST-style REST (/rest/v1/...) and GoTrue-style auth (/auth/v1/...). Use it in tests or local tooling without a real Supabase project—similar in spirit to an embedded mock, not a separate binary.
The server is implemented as MemorySupabaseHttpServer (request pipeline, shared MemoryStore + AuthMemory). createMemorySupabaseServer starts Node’s http.Server, resolves listen address and keys, wires Winston logging, and returns a handle you can close().
Requirements
- Node.js 20.9+ or 22+
- Peer dependency:
@supabase/supabase-js^2
Install
npm install supabase-in-memory-server @supabase/supabase-jsQuick start (programmatic)
import { createClient } from '@supabase/supabase-js';
import {
createMemorySupabaseServer,
applyMemorySupabaseEnvToProcess,
} from 'supabase-in-memory-server';
const srv = await createMemorySupabaseServer({ useEnv: false });
// Optional: expose the same values Supabase apps expect in process.env
applyMemorySupabaseEnvToProcess(srv);
const anon = createClient(srv.url, srv.anonKey, {
auth: { persistSession: false, autoRefreshToken: false },
});
const { error } = await anon.from('todos').insert({ id: 1, title: 'Buy milk' }).select();
// ...
await srv.close();Convenience: server + anon client
import { createTestSupabaseClient } from 'supabase-in-memory-server';
const { client, url, anonKey, serviceRoleKey, store, logger, close } =
await createTestSupabaseClient();
// `client` is already createClient(url, anonKey, { auth: { persistSession: false, ... } })
await close();Service role (admin) client
Use the service role key for auth.admin (e.g. listUsers). Never expose this key in browser bundles.
import { createClient } from '@supabase/supabase-js';
import { createMemorySupabaseServer } from 'supabase-in-memory-server';
const srv = await createMemorySupabaseServer({ useEnv: false });
const admin = createClient(srv.url, srv.serviceRoleKey, {
auth: { persistSession: false, autoRefreshToken: false },
});
const { data, error } = await admin.auth.admin.listUsers({ perPage: 50 });
await srv.close();What you get back from the server
createMemorySupabaseServer / startInMemorySupabaseServer resolve to an object with:
| Field | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| url | Base URL, e.g. http://127.0.0.1:<port> |
| anonKey | Anon API key (maps to SUPABASE_ANON_KEY) |
| serviceRoleKey | Service role / admin key (maps to SUPABASE_SERVICE_ROLE_KEY) |
| supabaseKey | Same as anonKey (legacy name) |
| store | MemoryStore — in-memory tables for REST |
| auth | AuthMemory — in-memory users and sessions |
| server | Node http.Server |
| logger | Winston Logger — lifecycle + per-request logs (see Logging below) |
| close() | Stops the server and clears auth state (tables are not cleared unless you call store.clearAll()) |
Logging and debug mode
Logging uses Winston. Each HTTP request gets a 10-character hex requestId. In debug mode you will see:
Incoming request—{ requestId, method, path, query }when the request enters the pipeline (including OPTIONS).Request finished—{ requestId, method, path }in afinallyblock when handling completes (success, early return, or error).- Handler-level
debuglines for auth and REST (correlated via a child logger that carriesrequestIdon every line).
Options
| Setting | Effect |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| debug: true in server options | Root log level debug: full request/handler detail. |
| debug: false (default) | Level info: listen/close and error / warn; routine traffic stays at debug (hidden unless you enable debug). |
| SUPABASE_JEST_MOCK_DEBUG | If 1, true, or yes, sets debug when useEnv is not false. |
await createMemorySupabaseServer({ useEnv: false, debug: true });To silence logs in noisy tests: srv.logger.silent = true.
You can also build a matching logger with createMemorySupabaseLogger(debug) if you construct MemorySupabaseHttpServer yourself (advanced).
Environment variables
By default (useEnv is not false), options are merged with the environment: env first, then explicit options override.
| Variable | Purpose |
| ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| SUPABASE_URL | If set, host and port for listening are taken from this URL (e.g. http://127.0.0.1:54321). |
| SUPABASE_JEST_MOCK_HOST | Overrides listen hostname after URL parsing. |
| SUPABASE_JEST_MOCK_PORT | Overrides listen port after URL parsing. Use 0 for an ephemeral port. |
| SUPABASE_ANON_KEY | Anon key used as the apikey header for the public client. |
| SUPABASE_SERVICE_ROLE_KEY | Service role key for admin operations. |
| SUPABASE_JEST_MOCK_DEBUG | Enables verbose debug logging (1, true, or yes). |
After the server is running, you can sync process.env to the actual URL and keys (recommended for apps that only read env):
import {
createMemorySupabaseServer,
applyMemorySupabaseEnvToProcess,
} from 'supabase-in-memory-server';
const srv = await createMemorySupabaseServer();
applyMemorySupabaseEnvToProcess(srv);
// process.env.SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEYCLI
From the package install:
npx supabase-in-memory-server- Starts the in-memory server (honors the same env vars as above).
- Writes
SUPABASE_URL,SUPABASE_ANON_KEY, andSUPABASE_SERVICE_ROLE_KEYintoprocess.env. - Prints a JSON object with those three keys to stdout.
- Logs the listen URL to stderr (and Winston lifecycle lines unless silenced).
- Exit with Ctrl+C (SIGINT) or SIGTERM.
Authentication and API keys
- Every request must include header
apikeyequal to eitheranonKeyorserviceRoleKey. Missing or wrong key → 401. GET /auth/v1/admin/usersrequires the service role key (orAuthorization: Bearer <serviceRoleKey>).
Implemented auth routes include: signup (email or phone), token (grant_type=password and refresh_token), logout, get user (GET /auth/v1/user), and admin list users (paginated). JWTs used by the mock are unsigned test tokens—do not use for real security.
REST (PostgREST-shaped)
Supported on /rest/v1/:table: insert, upsert (via Prefer: resolution=merge-duplicates / ignore-duplicates), select with common filters (eq, neq, gt, gte, lt, lte, is, in), order, limit / offset, update, delete, Prefer: return=representation, Prefer: count=…, and Accept: application/vnd.pgrst.object+json for .single() / .maybeSingle()-style behavior.
With Prefer: return=representation, PATCH returns the updated rows in the body: one row as a single JSON object, multiple rows as a JSON array (same convention as POST insert).
There is no RLS, no Realtime WebSocket, no Storage, no Edge Functions, no /rest/v1/rpc/..., and no full PostgREST operator set (see TODO below).
Publishing (maintainers)
GitHub Actions Release workflow (.github/workflows/release.yml) runs when you push a version tag:
- Set
versioninpackage.jsonto the release you want (e.g.1.2.3). - Commit that change on the default branch (e.g.
main). - Create and push a tag
v+ the same version:git tag v1.2.3 && git push origin v1.2.3.
The tag must be vMAJOR.MINOR.PATCH and match package.json exactly (e.g. tag v1.2.3 ↔ "version": "1.2.3"). The workflow runs typecheck, lint, format check, build, and tests, then npm publish --access public (needed for scoped package names) and creates a GitHub Release with generated notes.
Repository secret: add NPM_TOKEN in the repo’s Settings → Secrets and variables → Actions. Use an npm access token with permission to publish this package (granular token for @scope/package, or a user token with publish rights).
API reference (exports)
The main entry is supabase-in-memory-server (dist/index.js). Notable exports:
createMemorySupabaseServer,startInMemorySupabaseServer— start the HTTP server.createTestSupabaseClient— server +SupabaseClientwith anon key.applyMemorySupabaseEnvToProcess— setSUPABASE_*env from a running server.MemorySupabaseHttpServer— HTTP app class (advanced: custom lifecycle, tests).createMemorySupabaseLogger— Winston factory aligned with the server’s log format.readMemorySupabaseServerEnv,resolveMemorySupabaseServerOptions,generateDefaultProjectKeys,MemorySupabaseServerInputOptions— configuration helpers.MemorySupabaseServer,MemorySupabaseServerOptions,MemoryStore,AuthMemory,JsonRecord, etc.
See TSDoc on those symbols in the source for details.
TODO (toward a fuller in-memory Supabase mock)
This package intentionally covers a small, test-friendly slice of Supabase. A full in-memory mock would still need (non-exhaustive):
Auth (GoTrue)
- [ ] OAuth / SSO providers, magic links, OTP, and phone SMS flows.
- [ ] Additional
auth.adminAPIs:deleteUser,updateUserById,inviteUserByEmail,generateLink, etc. - [ ] MFA (TOTP, WebAuthn) and step-up semantics.
- [ ] Session management: list/revoke sessions, refresh rotation edge cases matching production.
- [ ] Password recovery / email change / user verification flows.
- [ ] Stricter alignment with GoTrue error payloads, status codes, and headers.
- [ ] Optional JWT signing/verification matching real projects (currently test-oriented tokens).
REST / PostgREST
- [ ] RPC:
POST /rest/v1/rpc/:function(and client.rpc()). - [ ] Embedded resources / foreign-table
selectshapes (profiles(*)). - [ ] More filter operators (
like,ilike,fts,cs,cd,@>,range,or,and,not, etc.). - [ ]
columns=, default values, generated columns behavior (as far as in-memory JSON allows). - [ ] Schemas / multiple schemas, views, and table exposure rules.
- [ ] Row Level Security simulation (policies per role / JWT claims).
- [ ] Transactions or batch semantics if clients rely on them.
- [ ] Closer error codes and Prefer / Content-Range behavior vs real PostgREST.
Realtime
- [ ] WebSocket server and
postgres_changes(and channel/auth parity with Supabase Realtime).
Storage
- [ ] S3-compatible or Supabase Storage REST: buckets, objects, signed URLs, MIME limits.
Edge Functions & platform
- [ ] Edge Functions invoke path or local stub.
- [ ] Database webhooks, pg_cron, and other backend-only features (usually out of scope for a pure HTTP mock).
Developer experience
- [ ] Optional OpenAPI / schema endpoint mimicking PostgREST.
- [ ] Snapshot / fixture helpers for common test scenarios.
Contributions that close items above are welcome; keep the default footprint small so Jest/CI stays fast.
Development
npm install
npm run build # emit dist/
npm run typecheck
npm run lint
npm run format:check
npm test # integration + e2e tests