REST from your database catalog
    DocumentationCheat sheetQuestionsDownloadSource
    Questions
    • How do I...?
    Start here
    • Getting started
    • What a table becomes
    • Compared with PostgREST
    Setting up
    • Tables, columns and relations
    • Security and hooks
    • Writes and procedures
    • Prefer: return=representation
    • Node, Fastify, Express
    Reading
    • select
    • /{key}
    • relation(...)
    • order
    • limit, offset
    • relation.order, .limit
    • count
    • count(), sum(), avg()...
    • "name"
    Filtering
    • eq, neq
    • gt, gte, lt, lte
    • in
    • like, ilike
    • match, imatch
    • is
    • ov, cs, cd
    • not
    • relation.column
    • or, and
    Databases
    • Supported databases
    • Adding a database
    Reference
    • Cheat sheet
    • Errors
    Setting up

    Security and hooks

    Who may ask, row security, tenants, and the limits on every request.

    restloom decides what can be asked. Your backend decides who may ask it, through hooks.

    What restloom enforces

    • Exposure: only the schemas and tables you name are served, and only their visible columns.
    • Sensitive columns: a column whose name looks like a secret is hidden without being asked, see below.
    • Bound values: every value is a bound parameter, checked against its column's type before any SQL runs. Names come from the catalog and are quoted; nothing from a request is written into SQL.
    • Bounded requests: every request is limited:
    LimitDefaultOption
    Rows per page50, at most 1000limits.page, limits.maxPage
    Relations walked by a path or an embed2limits.depth
    Filters, every or/and term included50limits.filters
    Items in one in list500limits.inList
    Rows in one write body1000limits.bodyRows
    Statement time5 slimits.timeoutMs

    A request over a limit is refused, never quietly cut down.

    • Errors: they answer stable codes. A database message never reaches the client. Unexpected errors answer internal and reach your onError hook.

    Sensitive columns

    restloom reads every column name when it reads the catalog. A name made of a word such as password, hash, salt, secret, token, api_key, private_key, otp, mfa or recovery, alone or between underscores, marks the column as sensitive: password_hash and reset_token are hidden, tokenizer is not. The full list is in Columns.

    A hidden column is not served at all. It cannot be read, filtered, sorted or written, select=* leaves it out, and naming it answers as a column that does not exist, so a client cannot even learn it is there:

    GET /users?select=name,password_hash
    400what unknown_column means
    { "error": "unknown_column", "message": "password_hash" }

    The check runs on names, so it guards against the column nobody thought about when the table grew. It does not know what a column holds: hide a sensitive column with a harmless name yourself, with hide.

    To serve a column the check hides on purpose, name it in show. It then behaves as any other column, and is writable only if write lists it:

    resources: {
        files: { show: ['sha256_hash'] },
        users: { hide: ['phone'] },
    }

    sensitive replaces the whole pattern, for every resource. Start from DEFAULT_SENSITIVE to add words rather than lose the defaults:

    import { createREST, DEFAULT_SENSITIVE } from '@crudy/restloom-postgres';
     
    createREST({
        db, // from a database package, see Dialects
        expose: { schemas: ['app'] },
        sensitive: new RegExp(`${DEFAULT_SENSITIVE.source}|(^|_)(iban|ssn)(_|$)`, 'i'),
    });

    Hooks

    import { createREST, RestError } from '@crudy/restloom-postgres';
     
    createREST({
        db, // from a database package, see Dialects
        expose: { schemas: ['app'] },
        hooks: {
            authorize(request, target) { /* throw a RestError to refuse */ },
            async transaction(tx, context) { /* session settings, role */ },
            scope(resource, context) { return { tenant_id: context.tenantId }; },
            onError(error, request) { log.error(error); },
        },
    });
    HookRunsUse it to
    authorize(request, target)before any SQL, with { resource, action } or { procedure, action: 'call' }refuse a request: throw new RestError(403, 'forbidden')
    transaction(tx, context)first, in every transactionset the role and session settings row security reads
    scope(resource, context)for every resource a query touches, embeds and relation filters includedadd equality filters, e.g. a tenant column, on databases without row security
    onError(error, request)for every answer of internallog it

    context is what your adapter builds from the request, usually the authenticated user. restloom never reads it.

    Row security

    Row security needs PostgreSQL, or SQL Server with security policies (see Dialects). With Postgres, it is the strongest perimeter: the database itself hides the rows a session may not see. Connect as a role without BYPASSRLS, and set what your policies read in the transaction hook:

    async transaction(tx, context) {
        await tx.query("SELECT set_config('app.user_id', $1, true)", [context.userId]);
    }

    Every read and write of the request then runs under those policies, embeds and relation filters included.

    Scope

    On a database without row security, scope keeps a request inside its perimeter. It returns column values a row must have, and restloom applies them:

    • Reads: every query on the resource, and every relation reaching it, only matches rows with those values.
    • Inserts: the scoped columns are filled in. A body giving another value is refused with out_of_scope.
    • Updates and deletes: only rows with those values are reached. An update moving a row out of scope is refused.
    PreviousTables, columns and relationsNext Writes and procedures
    restloom - a REST API generated from your database catalog
    DocumentationDownloadSource