crudy
    FeaturesUse CasesDocumentationPricingAboutContact
    Get started free
    Documentation
    • Introduction
    • Installation
    • Quickstart
    • CLI Reference
    • Overview
    • Core Packages
    • App Structure
    • Modules & Routes
    • Module Anatomy
    • Backend Module
    • Frontend Module
    • Quotas
    • Full Example
    • Overview
    • Docker
    • Environment Variables
    • Licensing
    Modules

    Quotas

    Declaring per-org record limits enforced by the framework.

    A quota caps how many rows of a module's data a single org can have. The cap is declared in the module's backend definition; the limit value comes from the license. The framework counts and enforces the cap before any POST reaches your code.

    Declaring a quota

    Add an entry to BackendModule.quotas:

    ts
    quotas: [
        {
            key: 'records',
            table: 'app.documents',
            orgColumn: 'org_id',
            routePath: '/documents',
            description: 'Maximum number of documents',
        },
    ]
    FieldPurpose
    keyIdentifier referenced by the license to declare the cap.
    tableFully qualified table whose rows are counted.
    orgColumnColumn on that table holding the owning org id.
    routePathThe exact route path whose POST requests are gated.
    descriptionHuman-readable description stored in app.modules.quota_defs.

    Identifier constraint

    table and orgColumn must match the regex ^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$. The framework validates this at boot and refuses to start otherwise. The check protects against SQL injection in the count query, since the values are interpolated into a SELECT count(*) FROM <table> WHERE <orgColumn> = $1.

    Warning

    Never compute a quota identifier from user input or runtime configuration. The regex is strict for a reason. If boot fails with quota ... has invalid identifier, fix the literal in your module source.

    How enforcement works

    The framework registers a preHandler on the routes that match routePath. For every POST:

    1. Read X-Org-Id from the request headers. If absent, respond 400 { error: 'X-Org-Id header required' }.
    2. Resolve the license's quota for (moduleId, orgId, key) using the org's ancestor chain.
    3. If the limit is null (unlimited), skip.
    4. Otherwise, count rows in table WHERE orgColumn = orgId.
    5. If used >= limit, respond 402 { error: 'quota_reached' }.
    6. Otherwise, let the request through.

    The count-then-insert is not atomic. If you need a hard guarantee under concurrency, add a database CHECK trigger that enforces the same cap.

    License limit values

    The license's scope.modules.<id>.limits.<key> value follows the same convention everywhere in Crudy:

    ValueMeaning
    key absent0 (blocked)
    nullunlimited
    non-negative integerexplicit cap

    Forgetting to declare a key in the license is never silently permissive. A module quota with no matching license key blocks every POST to the quota's route.

    Scope resolution

    When the preHandler resolves the limit for an org, it walks scopes in this order: exact match (scope.org_id === orgId), nearest ancestor with inherit: true, then the global default (scope.org_id === null). First match wins.

    Discoverability

    The quota declarations are upserted into app.modules.quota_defs on boot. The GET /license/quotas?orgId=<uuid> endpoint returns [{ moduleId, key, limit, used }] for the caller's reachable orgs. Modules can render quota meters in their pages by reading from that endpoint.

    When quotas are not enough

    A quota is a per-org record cap. It is not the right tool for:

    • Rate limiting. Use the framework's built-in rate limit or a Fastify plugin.
    • Per-user limits. The license model is org-scoped.
    • Spend or usage metering. Implement that in the module itself and report it through telemetry.
    PreviousFrontend ModuleNext Full Example
    crudy

    Build internal tools your teams actually use. Powered by a framework built for developers.

    Product

    • Features
    • Use Cases
    • Pricing

    Company

    • About
    • Contact

    © 2026 Crudy. All rights reserved.

    crudy.fr