npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aginix/adonis-rls

v1.0.1

Published

Row-level security for AdonisJS Lucid — scope Model.query() to the current actor by default, with explicit .sudo() escape

Readme

@aginix/adonis-rls

Row-level security for AdonisJS Lucid. Model.query() is scoped to the current actor by default — there's no opt-in step beyond composing one mixin and declaring a rlsScope. Bypass is explicit and named (.sudo(), optionally with a reason for audit hooks).

Status: v0.1 beta. API surface is design-locked but the package has not been deployed against a production database. Use behind a feature flag if you adopt it before 0.2.

Install

pnpm add @aginix/adonis-rls
node ace configure @aginix/adonis-rls

The configure hook publishes config/rls.ts and registers inject_actor_middleware on the router.

1. Make a model RLS-aware

// app/models/article.ts
import { compose } from '@adonisjs/core/helpers'
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { RowLevelSecurity, type RlsContext } from '@aginix/adonis-rls'
import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
import User from '#models/user'

export default class Article extends compose(BaseModel, RowLevelSecurity) {
  @column({ isPrimary: true })
  declare id: number

  @column()
  declare authorId: number

  @column()
  declare visibility: 'public' | 'private'

  static rlsScope(
    query: ModelQueryBuilderContract<typeof Article>,
    ctx: RlsContext<User>
  ) {
    const user = ctx.actor
    if (!user) return query.where('visibility', 'public') // anonymous
    if (user.role === 'admin') return query                // admin sees all
    return query.where((q) => {
      q.where('author_id', user.id).orWhere('visibility', 'public')
    })
  }
}

That's it. Every read goes through rlsScope automatically — Article.query(), Article.find(id), Article.findOrFail(id), Article.query().paginate(...), Article.query().preload('comments'). Mass update / delete / increment / decrement via the query builder are scoped too.

2. Run inside an actor context

Inside an HTTP handler, the middleware does this for you:

// HTTP handler — actor auto-injected via ctx.auth.user
await Article.query()       // scoped to ctx.auth.user
await Article.find(42)      // null if user can't see id=42

Outside HTTP (jobs, seeders, CLI), be explicit:

import { runWithActor } from '@aginix/adonis-rls'

await runWithActor(systemUser, async () => {
  await Article.query()
})

// or via the static helper:
await Article.forActor(systemUser)

3. Bypass (when you really need to)

await Article.sudo('payroll job: monthly summary')   // with a reason (recommended)
await Article.sudo()                                  // no reason -- fine for obvious call sites
await Article.query().sudo('admin export').where('status', 'archived')

reason is optional. When passed, it's preserved on the builder state for an upcoming audit layer; pass one whenever the call site isn't self-explanatory.

4. Preload cascades

.preload('comments') runs Comment's rlsScope automatically:

// Both Article and Comment have rlsScope. Both run.
await Article.query().preload('comments')

// Compose with user clauses inside the callback
await Article.query().preload('comments', (q) => q.where('approved', true))

// Mix scopes across the tree
await Article.sudo('admin export').preload('comments', (q) => q.forActor(viewer))

5. Config

// config/rls.ts
import { defineConfig } from '@aginix/adonis-rls'

export default defineConfig({
  // What happens when Model.query() runs without an actor (no middleware,
  // no .forActor, no .sudo). 'throw' (default) is safest. 'deny' returns
  // no rows. 'allow' skips the scope entirely — only opt in if you have
  // another authz layer.
  defaultBehavior: 'throw',

  // How to resolve the actor from HttpContext. Default reads ctx.auth?.user.
  // Override here if your auth shape is different. May be sync or async —
  // the middleware awaits the result, so you can do ctx.auth.check(),
  // verify a JWT, hit a session store, etc.
  // actorResolver: async (ctx) => {
  //   await ctx.auth.check()
  //   return ctx.auth.user ?? null
  // },
})

Errors

| Class | When | | -------------------------- | ------------------------------------------------------------------------------------- | | MissingActorError | Model.query() ran with no actor and defaultBehavior: 'throw'. | | RlsScopeNotDefinedError | Model composes RowLevelSecurity but doesn't declare static rlsScope. |

Composing with other packages

| Package | How | | ------------------------- | --------------------------------------------------------------------------------------- | | @aginix/adonis-vulcan | Model.query().filter(qs) — RLS applies first, vulcan's filter clauses AND on top. | | @aginix/adonis-object-id| Article.findByObjectId(ref) — routes through query(), so RLS hides invisible rows. | | BaseResourceController | All five actions are RLS-scoped automatically when the model uses the mixin. | | Bouncer | Complementary — Bouncer gates endpoints, RLS gates rows. Use both. |

Limitations (v0.1)

  • In-app TypeScript only; no Postgres-native CREATE POLICY layer (planned).
  • Row-level only; column-level visibility belongs to a future serializer/transformer concern.
  • Article.create({...}) does NOT run rlsScope (no rows to scope at INSERT time). Write authz is delegated to Bouncer.
  • Single actor type per app (config.actorResolver returns one shape).
  • Pivot tables in manyToMany preloads are not scoped — only the related model is.

License

MIT