Skip to content

See the security payoff

Run the reference Publish api, sign in as an admin then a member, and watch row-level scoping and email masking without a handler check.

Before you wire persistence into your own posts, see where the framework is heading. The reference Publish workspace ships auth and api fully built on users and orgs. In four curls you watch one JWT drive row-level scoping and field masking — no check written in any handler. By the end of this page an admin sees every column, a member sees the same rows with email gone, and a cross-tenant read returns 403.

The tutorial blog app has no auth yet — that is deliberate. To live the payoff now, drive the reference api app over users and orgs. It already wires the guards, the ability, and the masker you build by hand later in Authenticate and authorize.

  1. Clone the reference repo and open it in the devcontainer — VS Code offers Reopen in Container, which brings up Postgres and Redis beside the workspace and installs the nestrs CLI. The demo commits its whole configuration, dev keypair included, so there is nothing to prime before the first run.

    Terminal window
    $ git clone https://github.com/YV17labs/NestRS && code NestRS
    # accept "Reopen in Container", then in the container terminal:
    $ cd demo

    Without VS Code, start the same services from the repo’s compose file (docker compose -f .devcontainer/docker-compose.yml up -d postgres redis rustfs) and point NESTRS_SEAORM__URL / NESTRS_REDIS__URL at localhost in a git-ignored demo/.env.local.

  2. Load the demo tenants and start both apps.

    Terminal window
    $ nestrs run db reset
    INFO seed: seed complete inserted=12
    $ nestrs run dev auth # token issuer on :3001
    $ nestrs run dev api # resource server on :3002

    db reset seeds two orgs — Acme and Globex — each with an admin and members. The four accounts that can sign in share the password publish-demo; the rest are seeded without one, so a login attempt with any other address is refused.

  3. Sign in as the Acme admin and read the org’s users.

    Terminal window
    $ ADMIN=$(curl -s localhost:3001/login -H 'content-type: application/json' \
    -d '{"email":"admin@acme.test","password":"publish-demo"}' | jq -r .access_token)
    $ curl -s localhost:3002/users -H "authorization: Bearer $ADMIN" \
    | jq -c '.[0] | {id, name, email}'
    {"id":"00000000-0000-7000-8000-00000000ac00","name":"Acme Admin","email":"admin@acme.test"}

    The admin sees every field — email included — and only Acme rows.

  4. Sign in as a plain member and run the identical query.

    Terminal window
    $ MEMBER=$(curl -s localhost:3001/login -H 'content-type: application/json' \
    -d '{"email":"acme-user-1@example.test","password":"publish-demo"}' | jq -r .access_token)
    $ curl -s localhost:3002/users -H "authorization: Bearer $MEMBER" | jq -c '.[0]'
    {"id":"00000000-0000-7000-8000-00000000ac00","name":"Acme Admin"}

    Same rows, same route — but email is gone from every object. The member’s ability grants only id and name.

  5. Read a user in the other tenant with the member’s token.

    Terminal window
    $ curl -s -o /dev/null -w '%{http_code}\n' \
    localhost:3002/users/00000000-0000-7000-8000-000000006101 \
    -H "authorization: Bearer $MEMBER"
    403

    That row belongs to Globex, so the member’s org-scoped ability refuses it — a 403, decided by the framework, not the handler.

One policy — the caller’s Ability — drove three outcomes with no authorization code in the controller. The row-level .when(...) condition scoped both listings to the caller’s org; the field-level .fields([...]) grant dropped email for the member; and the by-id bind turned a cross-tenant load into a 403. The UsersController that produced all three is the same thin CRUD shape you are about to write for posts — it never reads the tenant or the role.

You build this yourself — the guards, the AuthzAbility, the Bind extractor — in Authenticate and authorize, once your posts feature persists and is tested.

  • Expose over HTTP — the next step: back in blog, write the service and mount the CRUD routes.