A practical data isolation strategy for teams sharing a production-connected database and a limited set of test accounts
Many startups run E2E tests against a QA environment that shares a production database and a handful of test accounts. This guide shows you how to write safe, parallelizable Playwright tests under those constraints — and how Stably makes the hard parts easy.
Create a setup project that logs in once and saves auth state. This avoids repeated login flows and keeps shared accounts as read-only identity providers.
auth/setup-auth.ts
Copy
import { test as setup } from '@playwright/test';setup('login as qa shared account', async ({ page }) => { await page.goto(process.env.BASE_URL!); await page.getByLabel('Email').fill(process.env.E2E_USER_EMAIL!); await page.getByLabel('Password').fill(process.env.E2E_USER_PASSWORD!); await page.getByRole('button', { name: 'Sign in' }).click(); await page.context().storageState({ path: 'playwright/.auth/qa-user.json' });});
Store credentials as Stably environment variables so you can manage E2E_USER_EMAIL and E2E_USER_PASSWORD per environment without touching code. Run tests with npx stably test --env QA to inject the right credentials automatically.
This fixture gives every test a collision-resistant namespace and tracks cleanup functions that run automatically after each test.
tests/fixtures/test-data.ts
Copy
import { test as base } from '@playwright/test';import { randomUUID } from 'crypto';type CleanupFn = () => Promise<void>;type TestDataFixture = { namespace: string; trackCleanup: (fn: CleanupFn) => void;};export const test = base.extend<TestDataFixture>({ namespace: async ({}, use, testInfo) => { const ns = [ 'e2e', randomUUID(), testInfo.project.name, String(testInfo.workerIndex), ].join('-'); await use(ns); }, trackCleanup: async ({}, use) => { const cleanupFns: CleanupFn[] = []; await use((fn: CleanupFn) => { cleanupFns.push(fn) }); // Run cleanup in reverse order (LIFO) for (const fn of cleanupFns.reverse()) { try { await fn(); } catch (error) { console.error('cleanup failed', error); } } },});export { expect } from '@playwright/test';
The UUID in the namespace prevents collisions even when Stably Cloud scales your tests to many parallel workers. ULID is also a good option if you want sortable IDs.
If a test must change workspace-level settings (billing, members, global toggles), isolate it in a serial suite so it never runs in parallel with other tests.
Copy
import { test } from '@playwright/test';test.describe.configure({ mode: 'serial' });
With the namespace fixture preventing collisions, you can safely scale parallelism. Start conservative and increase workers as you confirm tests stay collision-free.
Copy
# Start with a few workersnpx stably cloud test --project=e2e --workers=10# Scale up after collision-free runsnpx stably cloud test --project=e2e --workers=50
Stably Cloud supports up to 100 parallel workers, so once your data strategy is solid you can get fast feedback even on large suites.
Per-test cleanup should be your primary defense. But tests can crash, workers can be killed, and cleanup code can fail. Add a cleanup Playwright project that sweeps stale e2e-* records, and schedule it with Stably.Cleanup project:
tests/cleanup/stale-e2e-data.spec.ts
Copy
import { test, expect } from '@playwright/test';test('delete stale e2e data older than 24h', async ({ request }) => { const res = await request.post('/api/internal/cleanup/e2e', { data: { prefix: 'e2e-', olderThanHours: 24, }, }); expect(res.ok()).toBeTruthy();});
Setting autofix: true means if the cleanup test itself breaks (for example, due to an API change), Stably’s fix agent will automatically diagnose and repair it.You can also create and edit schedules visually from the Stably Scheduler UI.
Failed runs surface in the Stably dashboard with screenshots, traces, and AI-powered failure analysis — making it easy to tell whether a failure is a real bug, a stale data issue, or a test that needs updating.