Playwright can drive your browser. It can't test your backend.

Playwright is very good at what it does. If you need to drive a real browser, assert on what a user sees, and catch visual regressions, it is probably the best tool available right now.

This post is not an argument against it. It is about the specific class of bugs Playwright cannot see, why those bugs are the expensive ones, and what a test has to be able to do to catch them.

The test that passes while the system is broken

Here is a scenario most teams have lived through.

A user clicks "Publish". The button works. A success toast appears. Playwright asserts on the toast, the test goes green, CI passes, you ship.

Except the API gateway forwarded the request to the post service with a stale auth header. The post service accepted it anyway, wrote the row with a null author_id, and returned 201. The UI has no idea. It got a 201, so it showed a toast.

Your test asserted on the toast.

Nothing in that flow is Playwright's fault. It did exactly what it was asked to do: drive a browser and check what the user sees. The problem is that the bug was never visible in the browser. It was in the traffic between two services and in a row in Postgres.

Where Playwright's boundary actually sits

Playwright's model is the browser. Everything it does well flows from that.

It can navigate, click, type, wait for selectors, and screenshot. It can make API requests directly through its request context, which is genuinely useful for setup and teardown. It can intercept and mock network calls the browser makes.

What it does not do:

It does not run your services. Playwright assumes something is already serving your app. Getting your services, databases, and dependencies into a known state before the test runs is your problem to solve, usually with Docker Compose and a pile of shell scripts.

It cannot see traffic between services. Playwright's route interception works on requests the browser initiates. When your API gateway calls your post service, that call never touches the browser. It is invisible to the test.

It has no view of your database. You can assert that a page shows "My new post". You cannot assert that the row was written correctly, that the foreign key is populated, or that the write went to the right table.

Mocking third parties means running your own mock. To test a flow that calls Stripe or Auth0, you need to stand up a mock server, point your service at it, and manage its lifecycle. Playwright does not help here because the call happens server-side.

None of this is a criticism. Playwright is a browser automation library and it is excellent at being one. The issue is that most teams are using it as their entire integration testing strategy, and the browser is only one of four layers where things break.

The four layers

Any request in a modern application passes through roughly four layers:

Browser and frontend - what the user actually sees and clicks.

API and backend - the calls between your own services, the headers they carry, the payloads they send.

External APIs p Stripe, Auth0, Twilio, whatever you depend on that you do not control.

Database - what actually got written, and whether it is correct.

Most tools cover one or two of these well:

Each tool is good. The gap is that a single user action crosses all four layers, and no single tool follows it the whole way. So teams stitch tools together, and the seams between them are exactly where the untested behaviour hides.

What catching that bug actually requires

Go back to the publish flow. To catch the stale auth header and the null author_id, a test needs to do four things in the same run:

  1. Drive the browser through the publish flow.
  2. Capture the HTTP call from the web app to the API gateway and assert on its method, URL, and response status.
  3. Mock the Auth0 JWKS endpoint so the test does not depend on a live third party.
  4. Query Postgres directly and assert the row exists with the right values.

Doing all four with the usual stack means Playwright, a Docker Compose file, a mock server you maintain, and a database client wired into your test setup. It is possible. It is also brittle, and it is why most teams stop at step one.

What this looks like in Dokkimi

Dokkimi was built for exactly this shape of test. You declare your services, databases, and mocks in YAML, and it spins up an isolated Docker environment for every run: your services, a real database, a headless browser, and interceptor sidecars capturing every HTTP call between services.

The publish flow above becomes a single definition:

name: author-publish-flow
items:
  - $ref: ../shared/web-app.yaml
  - $ref: ../shared/api-gateway.yaml
  - $ref: ../shared/post-service.yaml
  - $ref: ../shared/postgres-db.yaml
  - $ref: ../shared/mock-auth0-jwks.yaml

tests:
  - name: Publish a new post
    steps:
      # Drive the browser through the publish flow
      - action:
          type: ui
          url: http://web-app:3000/posts/new
          subSteps:
            - action: type
              selector: '#title'
              value: 'My new post'
            - action: click
              selector: '[data-testid="publish-btn"]'
            - action: waitForSelector
              selector: '[data-testid="success-toast"]'
            - action: screenshot
              name: post-published
        assertions:
          # Assert on the traffic between services
          - match:
              path: $.traffic
              where:
                - path: $$.origin
                  operator: eq
                  value: web-app
                - path: $$.request.method
                  operator: eq
                  value: POST
                - path: $$.request.url
                  operator: contains
                  value: api-gateway/v1/posts
              count: 1
            assertions:
              - path: $.match.response.status
                operator: eq
                value: 201

      # Query the database directly
      - action:
          type: dbQuery
          database: postgres-db
          query: "SELECT title FROM posts WHERE title = 'My new post'"
        assertions:
          - count: $.response.data
            operator: eq
            value: 1

One file. Browser assertion, traffic assertion, mocked third party, and a real database query, all in the same run. The screenshot step also diffs against a baseline, so visual regressions are caught in the same pass.

Your services run unmodified. Dokkimi wires up the sidecars, routing, DNS, browser, and cleanup. There are no code changes on your side.

When Playwright is still the right answer

To be clear about where the line sits.

Use Playwright when you are testing a frontend against an API you do not need to inspect, when your app is largely a monolith with a straightforward database, or when the bugs you keep shipping are genuinely UI bugs.

Use Dokkimi when you run multiple services and the interesting failures happen between them, when you need to assert on database state as part of an end-to-end flow, when your tests depend on third-party APIs you would rather mock properly than stub, or when you have a staging environment that has quietly drifted from production.

Plenty of teams run both. Playwright for focused frontend suites, Dokkimi for the flows that cross service boundaries.

Try it

Dokkimi is free to run locally and in CI.

brew install dokkimi/tap/dokkimi
# or
npm install -g dokkimi

dokkimi init      # scaffolds a .dokkimi/ folder with examples
dokkimi validate  # check your definitions
dokkimi run       # spin up the environment and run

Requires Node 20+ and Docker. Run dokkimi doctor after installing to verify your setup.

If you use Claude Code, Cursor, or GitHub Copilot, Dokkimi ships an MCP server that auto-registers on install, so your agent can write, run, and debug test definitions as native tool calls.

The docs are at dokkimi.com/docs, and there is a working Next.js demo with Google OAuth, Postgres, and a full CI pipeline at github.com/dokkimi/demo-nextjs.