> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stably.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Isolation Strategy

> 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.

## Why This Is Hard

When your QA environment is backed by a production database you cannot:

* Spin up an isolated database per test run
* Create unlimited disposable users
* Drop tables or truncate data as cleanup

But you still need tests that run in parallel without stepping on each other, clean up after themselves reliably, and never touch real customer data.

<Warning>
  With a production-connected DB, treat every shared user and shared workspace as **read-only** unless a test creates a dedicated child entity first.
</Warning>

## The Core Contract

Every test must follow four rules:

<Steps>
  <Step title="Create its own data">
    Tests create the entities they need, prefixed with `e2e-<uuid>` for identification.
  </Step>

  <Step title="Mutate only what it created">
    Never edit or delete baseline entities like shared workspaces, users, or billing settings.
  </Step>

  <Step title="Clean up by exact IDs">
    Delete only by the specific IDs the test created — never by broad filters.
  </Step>

  <Step title="Schedule a safety net">
    Use [Stably Scheduler](/run-tests/schedulers) to sweep stale `e2e-*` records nightly.
  </Step>
</Steps>

## Recommended Architecture

<CardGroup cols={2}>
  <Card title="Shared Accounts = Identity Only" icon="user-lock">
    Reuse pre-created accounts for sign-in. Never modify account settings in parallel tests.
  </Card>

  <Card title="Data Is Test-Owned" icon="fingerprint">
    Every entity a test creates is namespaced with `e2e-<uuid>`, making it safe to delete.
  </Card>

  <Card title="Cleanup Is Deterministic" icon="broom">
    Delete by stored IDs in `afterEach`, not broad filters that could match real data.
  </Card>

  <Card title="Scheduler Sweeps Stragglers" icon="clock">
    A nightly Stably schedule runs a cleanup project that removes stale `e2e-*` data older than 24 hours.
  </Card>
</CardGroup>

## Playwright Implementation

### 1. Reuse auth state for shared accounts

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.

```ts auth/setup-auth.ts theme={null}
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' });
});
```

<Tip>
  Store credentials as [Stably environment variables](/stably2/environments) so you can manage `E2E_USER_EMAIL` and `E2E_USER_PASSWORD` per environment without touching code. Run tests with `npx stably --env QA test` to inject the right credentials automatically.
</Tip>

### 2. Namespace + cleanup tracker fixture

This fixture gives every test a collision-resistant namespace and tracks cleanup functions that run automatically after each test.

```ts tests/fixtures/test-data.ts theme={null}
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](/run-tests/run-tests-on-cloud) scales your tests to many parallel tests. `ULID` is also a good option if you want sortable IDs.

### 3. Use shared workspace, mutate only child data

The shared workspace acts as a read-only container. Tests create child resources inside it with the `e2e-` prefix and only ever touch those.

```ts tests/e2e/projects.spec.ts theme={null}
import { test, expect } from '../fixtures/test-data';

test.use({ storageState: 'playwright/.auth/qa-user.json' });

test('creates a child project in shared workspace safely', async ({ page, namespace, trackCleanup }) => {
  const workspaceId = process.env.E2E_SHARED_WORKSPACE_ID!;
  const projectName = `e2e-${namespace}`;

  await page.goto(`/workspaces/${workspaceId}/projects`);
  await page.getByRole('button', { name: 'New project' }).click();
  await page.getByLabel('Project name').fill(projectName);
  await page.getByRole('button', { name: 'Create' }).click();

  const projectId = await getProjectIdByName(page.request, workspaceId, projectName);
  trackCleanup(async () => {
    await deleteProjectById(page.request, workspaceId, projectId);
  });

  await expect(page.getByText(projectName)).toBeVisible();
});

async function getProjectIdByName(
  request: import('@playwright/test').APIRequestContext,
  workspaceId: string,
  name: string,
) {
  const res = await request.get(
    `/api/internal/workspaces/${workspaceId}/projects?name=${encodeURIComponent(name)}`,
  );
  const body = await res.json();
  return body.items[0].id as string;
}

async function deleteProjectById(
  request: import('@playwright/test').APIRequestContext,
  workspaceId: string,
  id: string,
) {
  await request.delete(`/api/internal/workspaces/${workspaceId}/projects/${id}`);
}
```

### 4. Serialize shared-state mutation tests

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.

```ts theme={null}
import { test } from '@playwright/test';

test.describe.configure({ mode: 'serial' });
```

## Running with Stably

### Organize with Playwright projects

Separate your E2E tests from your cleanup project so they can be triggered independently.

```ts playwright.config.ts theme={null}
import { defineConfig } from '@stablyai/playwright-test';

export default defineConfig({
  projects: [
    {
      name: 'setup',
      testMatch: '/auth/setup-auth.ts',
    },
    {
      name: 'e2e',
      dependencies: ['setup'],
      testDir: 'tests/e2e',
      use: { storageState: 'playwright/.auth/qa-user.json' },
      stably: {
        notifications: {
          slack: {
            channelName: '#test-results',
            notifyOnResult: 'failures-only',
          },
        },
      },
    },
    {
      name: 'cleanup',
      testDir: 'tests/cleanup',
      workers: 1,
      retries: 0,
    },
  ],
});
```

<Tip>
  Using `defineConfig` from `@stablyai/playwright-test` lets you configure [Stably notifications](/run-tests/alerting) per project while staying fully Playwright-compatible.
</Tip>

### Run on Stably Cloud

With the namespace fixture preventing collisions, you can safely scale parallelism. Start conservative and increase workers as you confirm tests stay collision-free.

```bash theme={null}
# Start with a few workers
npx stably cloud test --project=e2e --workers=10

# Scale up after collision-free runs
npx stably cloud test --project=e2e --workers=50
```

[Stably Cloud](/run-tests/run-tests-on-cloud) supports hundreds and thousands of parallel tests, so once your data strategy is solid you can get fast feedback even on large suites.

### Schedule cleanup as a safety net

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:**

```ts tests/cleanup/stale-e2e-data.spec.ts theme={null}
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();
});
```

**Nightly schedule in `stably.yaml`:**

```yaml stably.yaml theme={null}
schedules:
  nightly-e2e-cleanup:
    cron: "0 3 * * *"
    stablyTestArgs: "--project cleanup"
    timezone: "America/Los_Angeles"
    autofix: true
```

Setting `autofix: true` means if the cleanup test itself breaks (for example, due to an API change), Stably's [`fix` agent](/run-tests/autofix) will automatically diagnose and repair it.

You can also create and edit schedules visually from the [Stably Scheduler UI](/run-tests/schedulers).

### Monitor with alerts

Configure [Slack or email notifications](/run-tests/alerting) so your team knows immediately if cleanup fails or E2E tests start flaking:

```yaml stably.yaml theme={null}
schedules:
  nightly-e2e-cleanup:
    cron: "0 3 * * *"
    stablyTestArgs: "--project cleanup"
    timezone: "America/Los_Angeles"
    autofix: true

  morning-e2e-suite:
    cron: "0 9 * * 1-5"
    stablyTestArgs: "--project e2e --workers 30"
    timezone: "America/Los_Angeles"
```

Failed runs surface in the [Stably dashboard](https://app.stably.ai) 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.

## Guardrails

<AccordionGroup>
  <Accordion title="What to always do">
    * Prefix all test-created names with `e2e-<uuid>`
    * Delete by exact IDs stored during the test, never by broad queries
    * Keep one manual emergency cleanup command (`npx stably test --project cleanup`)
    * Increase workers gradually after confirming no collisions
    * Use [Stably environments](/stably2/environments) to manage credentials per environment
  </Accordion>

  <Accordion title="What to avoid">
    * Parallel write tests against the same shared entity
    * Reusing mutable fixture records across test files
    * Relying only on nightly cleanup (always clean per test first)
    * Depending on test execution order
    * Hardcoding customer IDs in tests
  </Accordion>
</AccordionGroup>

## Rollout Plan

<Steps>
  <Step title="Add the namespace + cleanup tracker fixture">
    Start with the fixtures above. No existing tests break since the fixture is opt-in.
  </Step>

  <Step title="Convert your flakiest mutable tests">
    Pick 3-5 tests that create or modify shared data and convert them to the create-and-own pattern.
  </Step>

  <Step title="Add the cleanup project and schedule it">
    Wire up the `cleanup` project in `playwright.config.ts` and add a nightly schedule in `stably.yaml`.
  </Step>

  <Step title="Serialize shared-state mutation tests">
    Move tests that must change workspace settings into `test.describe.configure({ mode: 'serial' })`.
  </Step>

  <Step title="Scale parallelism on Stably Cloud">
    Increase workers gradually via `npx stably cloud test --workers=N`. Monitor the Stably dashboard for collisions or cleanup failures.
  </Step>
</Steps>

## Related Docs

<CardGroup cols={2}>
  <Card title="Defining Test Groups" icon="layer-group" href="/use-cases/defining-test-groups">
    Organize tests into projects for flexible execution
  </Card>

  <Card title="Test Schedulers" icon="clock" href="/run-tests/schedulers">
    Create and manage scheduled test runs
  </Card>

  <Card title="Run Tests on Cloud" icon="cloud" href="/run-tests/run-tests-on-cloud">
    Scale test execution with Stably Cloud
  </Card>

  <Card title="Alerts & Notifications" icon="bell" href="/run-tests/alerting">
    Set up Slack and email alerts for test results
  </Card>
</CardGroup>
