Previously: In How to Set Up PGlite for End-to-End Testing, we bootstrapped a PGlite workspace, deployed a module, and wrote basic tests. Now let's add RLS policies and test them by switching user contexts—then wire up a WASM extension and run everything in CI.
Row-Level Security (RLS) is central to multi-tenant Postgres, and it behaves identically in PGlite and on a server. pglite-test creates the standard app roles for you, so you can switch into authenticated and verify policies without any manual setup.
In this lesson, you'll add RLS policies to your pets schema, test them across users, and add pgvector similarity search—all in-process.
Why Test RLS in PGlite?
Because it's the fastest way to prove your policies are correct. No server means no setup latency—each test spins up a fresh database in milliseconds, seeds data as the superuser, and switches into an app role to verify access. You write normal SQL; setContext() handles the role and JWT claims.
Adding RLS Policies
Add a change that introduces a user_id column and RLS policies, depending on the pets_table from the previous lesson:
Edit deploy/pets_rls.sql:
App Roles Are Ready
On a real server, roles like authenticated are bootstrapped when you provision the database. PGlite has no such step—it boots as a lone superuser—so pglite-test creates the same standard roles (anonymous, authenticated, administrator) for you before seeding. That means db.setContext({ role: 'authenticated' }) just works, with no manual CREATE ROLE.
Bringing your own roles? Pass
pglite: { roles: false }togetConnections()to boot a clean superuser-only instance and define your own roles/users inextensionSql. See thepglite-testdocs for the pattern.
Testing RLS
Create __tests__/pets-rls.test.ts:
Key pattern: seed with pg (superuser, bypasses RLS), isolate with pg.beforeEach()/afterEach(), and switch users with db.setContext(). In PGlite the pg and db clients share one in-process session, so hooks work exactly as they do with pgsql-test.
Context Switching Between Users
The power of the model is seamless context switching—each block authenticates as a different user:
Run the tests:
For faster feedback while writing tests, use watch mode:
Adding an Extension: pgvector
PGlite ships extensions as WASM modules. To use one—pgvector for similarity search, for example—you register it at construction and let your migration provision it. First install the extension package:
Add the extension to your module's .control requires line so pgpm knows about the dependency:
Add a change that creates the vector table (pgpm strips CREATE EXTENSION from migrations, so we provision it out-of-band in the test):
One instance deploys the whole module
Here's the key thing about seed.pgpm(): every suite deploys your module's entire plan, not just the change under test. Once documents_table is in the module, every suite—including the RLS one—deploys it, so every suite must register the vector WASM extension. Rather than repeat that config, extract a small shared helper in __tests__/connect.ts:
Point your RLS suite at it too—swap await getConnections() for await connect() (importing connect from ./connect). Now write the vector test with the same helper:
No native build, no server—the WASM extension loads in-process. (Loading a WASM extension on a cold runner is the slowest step, which is why the scaffold's jest.config.js sets a generous testTimeout—one place, no per-test timeouts to sprinkle around.)
Running in CI
Because there's no database to provision, CI is dramatically simpler than a server-based suite—no Postgres service, no Docker, no Supabase CLI. A complete workflow:
That's the headline win: the entire job is install + test. When you bootstrap with pgpm init workspace --pglite, this workflow is already in the scaffold.
Real-World Example
Want to see PGlite tests running in production CI? Check out the pglite-test-suite repository—a complete reference with an RLS demo, a pgvector demo, and a services-free GitHub Actions workflow. Its PGlite vs. pgsql-test doc catalogs every deviation from server-based testing.
Key Takeaways
- RLS behaves identically in PGlite and on a server—
setContext()sets role + JWT claims - Standard app roles are created for you—
authenticatedand friends are ready without manualCREATE ROLE(opt out withroles: falseto bring your own) pgseeds and isolates;dbswitches users—the same two-client model aspgsql-test- Every suite deploys the whole module—so shared extension setup belongs in one helper (
connect.ts), not repeated per file - Extensions are WASM modules—register with
extensions: { … }, provision withCREATE EXTENSIONinextensionSql, and declare them withpgpm extension --add - CI needs zero services—just
pnpm install && pnpm test
What's Next
You've tested real migrations, RLS policies, and a WASM extension entirely in-process. The pattern scales: any pgpm module that deploys on a server deploys in PGlite, so you can adopt it for fast local iteration and services-free CI without changing your SQL.
For advanced scenarios and the full list of PGlite-specific configuration, see the pglite-test-suite reference and the pglite-test documentation.

