Skip to content

Role-Based Access Control (RBAC) ​

Faucet provides fine-grained access control through roles, API keys, and per-table access rules. Access rules can also store row-level filters, but Faucet does not apply them to queries yet (see Row-level filters).

Architecture Overview ​

API Key ---> Role ---> Access Rules ---> Service/Table/Verb permissions
                                    \--> Row-level filters (stored only, not enforced yet)
  1. API Keys authenticate requests and are bound to a single Role
  2. Roles group access rules that define what operations are allowed
  3. Access Rules control which HTTP verbs are permitted on which service components
  4. Filters (optional) are saved with a rule and returned by the API, but are not yet applied to queries

Admin users authenticated via JWT bypass RBAC and have full access to all endpoints.

Verb Bitmask ​

Access rules use a bitmask to specify which HTTP methods are allowed. Each HTTP method maps to a power of 2:

HTTP MethodVerb ConstantValueDescription
GETVerbGet1Read records
POSTVerbPost2Create records
PUTVerbPut4Replace records
PATCHVerbPatch8Partial update records
DELETEVerbDelete16Delete records

Combine values by adding them together:

PermissionBitmaskCalculation
Read only1GET
Read + Create3GET + POST
Read + Update9GET + PATCH
Read + Create + Update11GET + POST + PATCH
Full CRUD31GET + POST + PUT + PATCH + DELETE

Examples ​

verb_mask: 1   -> GET only (read-only)
verb_mask: 3   -> GET + POST (read and create)
verb_mask: 5   -> GET + PUT (read and replace)
verb_mask: 9   -> GET + PATCH (read and partial update)
verb_mask: 17  -> GET + DELETE (read and delete)
verb_mask: 31  -> All operations (GET + POST + PUT + PATCH + DELETE)

Creating Roles ​

Via the Admin UI ​

Open Roles in the admin UI. With no roles yet, you can create Read-only on everything or Full access to everything with one click, or choose Create custom role. In the role editor, each access rule has three parts:

  • Database: one database, All databases (*), or a name pattern (prod_*).
  • Tables: leave empty for every table, or name one.
  • Access: Read only (GET), Read and write (GET, POST, PUT, PATCH), Full (all verbs including DELETE), or Custom.

What this role allows sums up the rules in plain words while you edit.

The role editor granting read access to every table in the shop database

Rules on all tables also cover schema changes

A rule whose tables are left empty matches _schema as well as _table. A Read and write or Full rule therefore also allows creating, altering and (with Full) dropping tables through /_schema. To limit a key to rows only, type _table/* as the table pattern.

Via CLI ​

bash
faucet role create --name readonly --verbs GET --description "Read-only access to all services"
faucet role create --name editor --verbs GET,POST,PUT,PATCH --description "Read and write access"
faucet role create --name admin --verbs '*' --description "Full access"

# Add more rules later
faucet role grant --role editor --service mydb --component "_table/orders" --verbs DELETE

A role created without --verbs has no rules and denies every request until you grant one.

Via API ​

bash
curl -X POST http://localhost:8080/api/v1/system/role \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "readonly",
    "description": "Read-only access to all tables",
    "access": [
      {
        "service_name": "*",
        "component": "_table/*",
        "verb_mask": 1
      }
    ]
  }'

Access Rules ​

Each role contains an access array of rules. Each rule specifies:

FieldTypeDescription
service_namestringDatabase service name (or * for all services)
componentstringAPI component path (e.g., _table/*, _table/users, _schema/*, _proc/*)
verb_maskintegerBitmask of allowed HTTP verbs
requestor_maskintegerSource of request (1=API, 2=Script, 4=Admin)
filtersarrayRow-level filters (optional; stored and returned, not yet enforced)
filter_opstringHow to combine filters: AND or OR

Component Patterns ​

The component field specifies which API endpoint the rule applies to:

PatternMatches
_table/*All tables
_table/usersOnly the users table
_table/ordersOnly the orders table
_schema/*All schema operations
_proc/*All stored procedures
_proc/calculate_totalOnly the calculate_total procedure

Role Examples ​

Read-only access to all tables across all services:

json
{
  "name": "readonly",
  "description": "Read-only access",
  "access": [
    {
      "service_name": "*",
      "component": "_table/*",
      "verb_mask": 1
    }
  ]
}

Full CRUD on specific tables in one service:

json
{
  "name": "orders_manager",
  "description": "Manage orders and order_items tables",
  "access": [
    {
      "service_name": "mydb",
      "component": "_table/orders",
      "verb_mask": 31
    },
    {
      "service_name": "mydb",
      "component": "_table/order_items",
      "verb_mask": 31
    },
    {
      "service_name": "mydb",
      "component": "_table/products",
      "verb_mask": 1
    }
  ]
}

Read-only access with schema viewing:

json
{
  "name": "analyst",
  "description": "Read data and view schemas",
  "access": [
    {
      "service_name": "*",
      "component": "_table/*",
      "verb_mask": 1
    },
    {
      "service_name": "*",
      "component": "_schema/*",
      "verb_mask": 1
    }
  ]
}

Row-level filters (stored, not yet enforced) ​

Not enforced yet

Filters are saved on an access rule and returned by the API, but Faucet does not yet apply them to queries. Do not rely on them to isolate tenants; use separate roles/services or database views instead.

Access rules can include filters intended to restrict which rows a role can access. Applying them to queries as additional WHERE conditions is planned. Today, a role with a filter still sees and changes every row its verb permissions allow.

To limit a key to a subset of rows today, create a database view (or a dedicated database user and schema) that contains only those rows, connect it as its own service or table, and grant the role access to that view only.

Filter Structure ​

Each filter has three fields:

FieldTypeDescription
namestringColumn name
operatorstringComparison operator (=, !=, >, <, >=, <=, LIKE, IN, IS NULL, IS NOT NULL)
valuestringValue to compare against

Filter Examples ​

These examples show the stored format. None of them restricts rows until filter enforcement ships.

Intended: restrict to one tenant's records (not enforced yet):

json
{
  "name": "tenant_user",
  "description": "Can only see records for their tenant",
  "access": [
    {
      "service_name": "mydb",
      "component": "_table/*",
      "verb_mask": 31,
      "filters": [
        {
          "name": "tenant_id",
          "operator": "=",
          "value": "42"
        }
      ]
    }
  ]
}

Intended: restrict to active records only (not enforced yet):

json
{
  "name": "active_only",
  "description": "Can only see non-deleted records",
  "access": [
    {
      "service_name": "mydb",
      "component": "_table/*",
      "verb_mask": 1,
      "filters": [
        {
          "name": "deleted_at",
          "operator": "IS NULL",
          "value": ""
        },
        {
          "name": "is_active",
          "operator": "=",
          "value": "true"
        }
      ],
      "filter_op": "AND"
    }
  ]
}

Intended: restrict by region (not enforced yet):

json
{
  "name": "us_east_reader",
  "description": "Read access to US-East region data",
  "access": [
    {
      "service_name": "mydb",
      "component": "_table/orders",
      "verb_mask": 1,
      "filters": [
        {
          "name": "region",
          "operator": "IN",
          "value": "'us-east-1','us-east-2'"
        }
      ]
    }
  ]
}

Combining Filters ​

The filter_op field records how multiple filters are meant to be combined:

  • AND (default): All filters must match
  • OR: Any filter can match

API Keys ​

API keys are the primary authentication mechanism for non-admin API consumers. Each key is bound to exactly one role.

Key Format ​

API keys follow the format: faucet_ followed by 64 hex characters (32 random bytes).

faucet_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef12345678

Security ​

  • The plaintext key is shown exactly once when created
  • Only a SHA-256 hash is stored in the database
  • The key prefix (first 15 characters) is stored for identification
  • Keys can have an optional expiration date
  • Revoked keys are immediately deactivated

Creating API Keys ​

In the admin UI, open API keys and click Create key. Faucet shows the key once, together with curl, JavaScript and Claude Code snippets that already contain it. See Admin UI → API keys.

A newly created key with ready-to-run snippets

Via CLI:

bash
faucet key create --role readonly --label "CI pipeline"

Via API:

bash
curl -X POST http://localhost:8080/api/v1/system/api-key \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "role_id": 2,
    "label": "CI pipeline",
    "expires_at": "2026-01-01T00:00:00Z"
  }'

Response (save the api_key value immediately):

json
{
  "id": 1,
  "api_key": "faucet_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef12345678",
  "key_prefix": "faucet_a1b2c3d",
  "label": "CI pipeline",
  "role_id": 2,
  "is_active": true,
  "created_at": "2025-01-15T10:30:00Z"
}

Listing API Keys ​

bash
faucet key list
# or
faucet key list --json

Revoking API Keys ​

Via CLI:

bash
faucet key revoke faucet_a1b2c3d

Via API:

bash
curl -X DELETE http://localhost:8080/api/v1/system/api-key/1 \
  -H "Authorization: Bearer $TOKEN"

Using API Keys ​

Include the key in the X-API-Key header:

bash
curl http://localhost:8080/api/v1/mydb/_table/users \
  -H "X-API-Key: faucet_a1b2c3d4e5f67890abcdef..."

Requestor Mask ​

The requestor_mask field on access rules controls which type of caller can use the rule:

ValueConstantDescription
1RequestorAPIExternal API calls (via API key)
2RequestorScriptServer-side scripts
4RequestorAdminAdmin panel requests

Combine values to allow multiple requestor types. For example, requestor_mask: 5 allows both API and Admin requests (1 + 4).

Complete RBAC Setup Example ​

Here is a full example setting up RBAC for an application with an analytics dashboard, a backend, and a per-tenant client. Because row-level filters are not enforced yet, the tenant role below is limited only by its verbs and tables; point it at a tenant-specific view or service for real isolation.

bash
# 1. Create roles
# Read-only role for analytics dashboards
curl -X POST http://localhost:8080/api/v1/system/role \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "analytics",
    "description": "Read-only access for analytics dashboards",
    "access": [
      {"service_name": "production", "component": "_table/*", "verb_mask": 1}
    ]
  }'

# Full CRUD role for the application backend
curl -X POST http://localhost:8080/api/v1/system/role \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "app_backend",
    "description": "Full CRUD for application",
    "access": [
      {"service_name": "production", "component": "_table/*", "verb_mask": 31},
      {"service_name": "production", "component": "_proc/*", "verb_mask": 3}
    ]
  }'

# Tenant role (the filter is stored but not enforced yet)
curl -X POST http://localhost:8080/api/v1/system/role \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "tenant_42",
    "description": "Scoped to tenant 42",
    "access": [
      {
        "service_name": "production",
        "component": "_table/*",
        "verb_mask": 11,
        "filters": [{"name": "tenant_id", "operator": "=", "value": "42"}]
      }
    ]
  }'

# 2. Create API keys for each role
curl -X POST http://localhost:8080/api/v1/system/api-key \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role_id": 1, "label": "Analytics dashboard"}'

curl -X POST http://localhost:8080/api/v1/system/api-key \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role_id": 2, "label": "App backend server"}'

curl -X POST http://localhost:8080/api/v1/system/api-key \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role_id": 3, "label": "Tenant 42 mobile app"}'