Postgres has no separate notion of "users" and "groups" — there is one thing, a role, and access control is built entirely out of granting privileges to roles. This lesson covers the two layers: table- and schema-level privileges with GRANT/REVOKE, and row-level security, where a policy decides which rows each role may see.
The seed is a shared document store: a documents table where each row has an owner (a role name). Two people, alice and bob, keep their notes in the same table.
SELECT * FROM documents ORDER BY id;
Roles are users and groups at once
A role is just a named grantee. Give it LOGIN and it can connect — that's what we informally call a "user." Leave LOGIN off and it's effectively a "group": nobody logs in as it, but you can grant privileges to it and then grant membership in it to other roles. Same object, different use.
Creating and wiring up roles needs the CREATEROLE attribute (or superuser), which your sandbox role doesn't have — so these are read-only snippets, not runnable blocks:
-- A login role (a "user") and a group role (no LOGIN)
CREATE ROLE alice LOGIN PASSWORD 'secret';
CREATE ROLE analysts;
-- Membership: alice now inherits everything granted to analysts
GRANT analysts TO alice;
Grant a privilege to analysts and every member gets it — that's how you manage access by team instead of per person. One special role, PUBLIC, means every role, present and future. Granting to PUBLIC is convenient and dangerous: it's the usual reason a table is readable by accounts you forgot existed.
Object privileges: GRANT and REVOKE
Privileges on a table are handed out with GRANT and taken back with REVOKE. You can do this on tables you own without any special attribute, so these run. Grant PUBLIC read and write, then immediately think better of the write: