One database. Four users. Four different views of the same data.
This demo showcases EYWA's Row-Level Security (RLS) in action, demonstrating how data access is automatically filtered based on user identity - even across dataset boundaries.
| User | Role | Password | What They Can See |
|---|---|---|---|
| Alice | Project Owner | password |
Everything in Project Alpha |
| Bob | Project Owner | password |
Everything in Project Beta |
| Charlie | Team Member | password |
Tasks assigned to him + group tasks |
| Diana | Consultant | password |
Her allocation... but not the task it points to |
Diana is the key demo case - she proves RLS works across dataset boundaries.
Linux / macOS:
curl -s https://s3.eu-central-1.amazonaws.com/eywa.cli/install_eywa_cli.sh | bashWindows (PowerShell):
Invoke-WebRequest -Uri "https://s3.eu-central-1.amazonaws.com/eywa.cli/install_eywa_cli.ps1" -OutFile eywa_cli_install.ps1
./eywa_cli_install.ps1For detailed installation instructions, see the EYWA Documentation
Ensure EYWA server is running on localhost:8080 with:
- Datasets deployed:
Project_ManagementandResource_Planning - Demo data loaded (users, projects, tasks, allocations)
Before starting a user REPL, connect to EYWA from that user's folder:
cd repl/alice
eywa connect http://localhost:8080
# Complete the device code flow - login as alice/passwordThis authenticates and stores tokens in the local eywa.edn file.
Each user has their own REPL session in repl/<user>/:
# Terminal 1 - Alice
cd repl/alice && eywa run -c "bb nrepl-server"
# Terminal 2 - Diana
cd repl/diana && eywa run -c "bb nrepl-server"
# ... etc for bob, charlieConnect your editor to the nREPL port shown, then:
;; MANDATORY - establish connection
(require '[eywa.client])
(eywa.client/open-pipe)
;; Load the demo namespace
(require '[demo.diana :as diana] :reload)Diana is a consultant. She has an allocation record - 10 hours billed to a task. But she's not assigned to that task, doesn't own the project, and isn't in any group.
Run as Diana:
(diana/step-1-projects)
;; => {:searchProject []}
;; Diana sees NO projects
(diana/step-2-tasks)
;; => {:searchProjectTask []}
;; Diana sees NO tasks
(diana/step-3-allocations)
;; => {:searchAllocation [{:hours 10,
;; :notes "Consulting - Diana on Alpha",
;; :user {:name "diana"},
;; :user_task nil}]} ;; <-- THE KEY!The AHA moment: Diana can see her allocation EXISTS (hours, notes visible), but :user_task is null. The Task entity has RLS guards, and Diana doesn't pass any of them.
This proves RLS works across dataset boundaries. The Allocation lives in Resource_Planning, but when it tries to resolve user_task (which points to Project_Management), the Task guards kick in and block access.
Charlie is assigned to tasks and is a member of the Engineering group.
Run as Charlie:
(charlie/step-1-projects)
;; => Projects where Charlie is a member
(charlie/step-2-tasks)
;; => Tasks assigned to Charlie directly OR via Engineering group
(charlie/step-3-allocations)
;; => Allocations WITH full task details visible
;; (contrast with Diana who sees user_task: nil)
(charlie/step-4-group-access)
;; => Alpha Task 3 visible via Engineering group
;; RLS follows: Task.assignee_group → UserGroup.users → UserKey insight: Charlie sees task details in his allocations because he passes the Task guards. Diana doesn't.
Alice owns Project Alpha. She has full control within her domain.
Run as Alice:
(alice/step-1-projects)
;; => Only Project Alpha (she owns it)
(alice/step-2-tasks)
;; => ALL Alpha tasks, regardless of who's assigned
(alice/step-3-cross-project-isolation)
;; => Cannot see Project Beta or its tasks
(alice/step-4-grant-access)
;; => Successfully adds Charlie as project member
;; (owners can modify their own projects)Key insight: Ownership gives full access within domain, but zero access outside it.
Bob owns Project Beta. He demonstrates complete isolation between peer owners.
Run as Bob:
(bob/step-1-projects)
;; => Only Project Beta
(bob/step-2-tasks)
;; => Only Beta tasks
(bob/step-3-isolation)
;; => THROWS: "Write access denied by RLS"
;; Bob cannot modify Alice's projectKey insight: Two owners, two domains, zero cross-contamination.
┌─────────────────────────────────────────────────────────────────────┐
│ PROJECT MANAGEMENT DATASET │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌─────────────────┐ ┌──────────┐ │
│ │ User │◄────────│ Project │────────►│ User │ │
│ │ (owner) │ owner │ │ members │ (members)│ │
│ └──────────┘ └────────┬────────┘ └──────────┘ │
│ │ │
│ │ tasks │
│ ▼ │
│ ┌─────────────────┐ │
│ │ ProjectTask │ │
│ │ │ │
│ │ • assignee ────┼──────────► User │
│ │ • assignee_group ─────────► UserGroup │
│ └────────┬────────┘ │ │
│ │ │ users │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ │ User │ │
│ │ └──────────┘ │
└─────────────────────────────────┼───────────────────────────────────┘
│
│ user_task (cross-dataset reference)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ RESOURCE PLANNING DATASET │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ Allocation │ │
│ │ │ │
│ │ • hours │ │
│ │ • notes │ │
│ │ • user ────────┼──────────► User │
│ │ • user_task ───┼──────────► ProjectTask │
│ └─────────────────┘ (guarded!) │
│ │
└─────────────────────────────────────────────────────────────────────┘
| Type | Example | Path |
|---|---|---|
| Direct Reference | Project owner | Project.project_owner → User |
| N:N Relation | Project members | Project.members → User |
| Multihop | Group assignment | Task.assignee_group → UserGroup.users → User |
| Cross-Dataset | Allocation → Task | Guards apply even when accessed from another dataset |
Project:
├── Guard 1: members → User [READ]
└── Guard 2: project_owner → User [READ, WRITE, DELETE]
ProjectTask:
├── Guard 1: assignee → User [READ]
├── Guard 2: assignee_group.users [READ] (multihop)
└── Guard 3: project.project_owner [READ, WRITE, DELETE]
- RLS is declarative - Define guard paths once, enforcement is automatic
- Cross-dataset boundaries respected - Guards apply even when data is accessed via foreign keys from another dataset
- Dynamic access - Add user to group/members = instant access change (no code deploy)
- Aggregates respect RLS -
_agg,_countonly compute over authorized rows - Write guards - Same mechanism protects mutations, not just reads
"Write access denied by RLS"
- Check if user is project owner (only owners can write)
- Verify you're logged in as the correct user (
eywa.ednin REPL folder)
Empty results when expecting data
- Run
(eywa.client/open-pipe)to establish session - Check if data was loaded by running as admin first
user_task is null (expected for Diana, not for Charlie)
- Diana: Correct - she doesn't pass Task guards
- Charlie: Check if he's assigned to the task or in the assignee_group
demo/
├── src/demo/
│ ├── core.clj # GraphQL wrappers, dataset import
│ ├── data.clj # Stable EUUIDs, demo entities
│ ├── alice.clj # Owner demo
│ ├── bob.clj # Isolation demo
│ ├── charlie.clj # Team member demo
│ └── diana.clj # Cross-dataset RLS demo (KEY!)
├── repl/
│ ├── admin/ # Superuser session
│ ├── alice/ # Alice's session
│ ├── bob/ # Bob's session
│ ├── charlie/ # Charlie's session
│ └── diana/ # Diana's session
└── resources/
└── *.json # Dataset definitions