$fastcrud

FastCRUD Documentation

FastCRUD is a multi-tenant backend that lets projects query their own external databases through a unified REST and GraphQL API. All data endpoints require authentication via a Bearer token tied to a specific project.


Table of Contents


API Regions

FastCRUD CRUD API endpoints are region-specific. Use the hostname closest to your database for the lowest latency.

RegionHostnameLocation
EU France 1eu-fr-1.fastcrud.devParis, France

All /authenticate/*, /crud/*, /graphql, and /query/* requests go to your region's hostname. The dashboard (dashboard.fastcrud.dev) is separate and region-independent.

Base URL example

https://eu-fr-1.fastcrud.dev/authenticate/crud/<accessKeyID>
https://eu-fr-1.fastcrud.dev/crud/users?limit=10
https://eu-fr-1.fastcrud.dev/graphql

More regions will be added in the future. If you need a specific region, contact support.


Supported Databases

FastCRUD supports five database backends. The dbType field in the connection payload selects which one to use.

dbTypeDatabaseJOIN supportNotes
0PostgreSQLYesDefault; uses pgx driver
1OracleYesTested with Oracle 12c+
2MySQLYesUses ? placeholders
3SQL ServerYesUses @p1 placeholders
4MongoDBNoCollections introspected by sampling one document

Connection string formats

Each database type builds its connection string from the same host, port, database, username, and password fields you POST to /connection.

DatabaseInternal format
PostgreSQLpostgresql://user:pass@host:port/db?sslmode=require
MySQLuser:pass@tcp(host:port)/db
SQL Serversqlserver://user:pass@host:port?database=db
Oracleoracle://user:pass@host:port/service
MongoDBmongodb://user:pass@host:port/db

For MongoDB, the database field is the database (not service) name. Schema introspection samples one document per collection to infer field types — empty collections will have no columns until a document is inserted and introspection is re-triggered.


Authentication

All /crud/* and /graphql endpoints require a project Bearer token.

Obtain a token

POST /authenticate/crud/:accessKeyID

ParameterInTypeDescription
accessKeyIDpathUUIDYour project's access key ID

No request body required.

Response 200 OK

{
  "access_token": "Bearer eyJhbGciOi...",
  "duration_millis": 86400000
}

Use the access_token value directly as the Authorization header on every subsequent request.

Authorization: Bearer eyJhbGciOi...

Tokens expire after the duration shown in duration_millis. Re-authenticate with the same endpoint to get a new token.


Filter Syntax

Filters are used in both REST query parameters and GraphQL arguments. They follow a consistent format:

column.operator.value

Operators

OperatorSQL equivalentExample
eq=status.eq.active
neq!=status.neq.deleted
gt>age.gt.18
gte>=age.gte.18
lt<price.lt.100
lte<=price.lte.100
likeLIKEname.like.John%

Combining conditions

Use AND / OR (or their shorthand && / ||) to chain conditions with a space between tokens:

age.gt.18 AND status.eq.active
age.gt.18 AND status.eq.active OR role.eq.admin
age.gt.18 && status.eq.active
age.gt.18 || role.eq.admin

Columns used in filters are validated against the project's introspected schema. Unknown columns return an error.


REST API

All REST endpoints share these headers:

Authorization: Bearer <token>
Content-Type: application/json

GET /crud/:table

Fetch all columns from a table.

Path parameters

ParameterTypeDescription
tablestringTable name

Query parameters

ParameterTypeDefaultDescription
filterstring—Filter expression (see Filter Syntax)
limitint1000Max rows to return (capped at 1000)
offsetint0Number of rows to skip

Example

GET /crud/users?limit=10&offset=0&filter=age.gt.18 AND status.eq.active
Authorization: Bearer eyJhbGciOi...

Response 200 OK

[
  { "id": 1, "name": "Alice", "age": 25, "status": "active" },
  { "id": 2, "name": "Bob",   "age": 30, "status": "active" }
]

GET /crud/:table/:columns

Fetch specific columns from a table.

Path parameters

ParameterTypeDescription
tablestringTable name
columnsstringComma-separated list of column names

Query parameters — same as above (filter, limit, offset)

Example

GET /crud/users/id,name,email?limit=5&filter=status.eq.active
Authorization: Bearer eyJhbGciOi...

Response 200 OK

[
  { "id": 1, "name": "Alice", "email": "[email protected]" },
  { "id": 2, "name": "Bob",   "email": "[email protected]"   }
]

Column names are validated against the introspected schema. Requesting a column that does not exist returns an error.


POST /crud/:table

Insert one or more rows into a table. The request body can be a single JSON object or a JSON array of objects. Column names must match the introspected schema.

Path parameters

ParameterTypeDescription
tablestringTable name

Request body — single row

{ "name": "Alice", "age": "30", "email": "[email protected]", "status": "active" }

Request body — multiple rows

[
  { "name": "Alice", "age": "30", "email": "[email protected]" },
  { "name": "Bob",   "age": "25", "email": "[email protected]"   }
]

Only include columns you want to set. Omitted columns use database defaults or NULL.

Response 200 OK

{ "rowsInserted": 2 }

PUT /crud/:table

Update columns on rows matching a filter. The request body is a JSON object of column→value pairs to set. The filter is provided as a query parameter.

Path parameters

ParameterTypeDescription
tablestringTable name

Query parameters

ParameterTypeRequiredDescription
filterstringNoFilter expression

Warning: omitting filter updates every row in the table.

Request body

{ "status": "inactive", "updated_at": "2024-06-01" }

Example

PUT /crud/users?filter=id.eq.42
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{ "name": "Alice Smith", "status": "active" }

Response 200 OK

{ "rowsAffected": 1 }

DELETE /crud/:table

Delete rows matching a filter. Hard capped at 1000 rows per request.

Path parameters

ParameterTypeDescription
tablestringTable name

Query parameters

ParameterTypeRequiredDescription
filterstringNoFilter expression

Warning: omitting filter deletes up to 1000 rows from the table.

Example

DELETE /crud/sessions?filter=expires_at.lt.2024-01-01
Authorization: Bearer eyJhbGciOi...

Response 200 OK

{
  "rowsAffected": 42
}

GraphQL API

The GraphQL endpoint provides a single entry point for all queries and mutations. The schema is dynamically generated from the project's introspected table structure, so the available fields reflect the exact tables and columns in your database.

Endpoint

POST /graphql

Headers

Authorization: Bearer <token>
Content-Type: application/json

Request body

{
  "query": "...",
  "variables": {},
  "operationName": "OptionalName"
}

Response envelope

{
  "data": { ... },
  "errors": [ ... ]
}

Query — fetch rows

One query field is generated per table. Each field returns a list of typed objects with fields matching the table's columns.

Signature

tableName(
  filter:  String,
  columns: [String],
  limit:   Int,
  offset:  Int
): [TableType]

Arguments

ArgumentTypeDefaultDescription
filterString—Filter expression (see Filter Syntax)
columns[String]allColumns to return; omit to return all
limitInt1000Max rows (capped at 1000)
offsetInt0Rows to skip

Fetch all columns

{
  "query": "{ users { id name email created_at } }"
}

With filter and pagination

{
  "query": "{ users(filter: \"age.gt.18 AND status.eq.active\", limit: 20, offset: 40) { id name age } }"
}

Select specific columns

{
  "query": "{ users(columns: [\"id\", \"email\"], limit: 50) { id email } }"
}

Query — join tables

The special join field lets you query across multiple tables with SQL JOINs. It returns a list of JSON objects (flat maps) because the column set is dynamic.

Signature

join(
  from:    String!,
  joins:   [JoinInput!]!,
  columns: [String],
  filter:  String,
  limit:   Int,
  offset:  Int
): [JSON]

JoinInput type

FieldTypeRequiredDescription
tableStringYesThe table to join
onStringYesJoin condition: table1.col1.operator.table2.col2
typeStringNoINNER (default), LEFT, or RIGHT

Join condition format

table1.column1.operator.table2.column2

Example: orders.user_id.eq.users.id → "orders"."user_id" = "users"."id"

Uses the same operators as filters (eq, neq, gt, gte, lt, lte, like).

Column selection in joins

Use table.column notation to select table-qualified columns and avoid ambiguity:

"orders.id", "orders.total", "users.name"

Simple INNER JOIN

{
  "query": "{ join(from: \"orders\", joins: [{ table: \"users\", on: \"orders.user_id.eq.users.id\" }], columns: [\"orders.id\", \"orders.total\", \"users.name\", \"users.email\"], limit: 50) }"
}

LEFT JOIN with filter

{
  "query": "{ join(from: \"orders\", joins: [{ table: \"users\", on: \"orders.user_id.eq.users.id\", type: \"LEFT\" }], columns: [\"orders.id\", \"orders.total\", \"users.name\"], filter: \"orders.total.gt.100\", limit: 25, offset: 0) }"
}

Multiple JOINs

{
  "query": "{ join(from: \"orders\", joins: [{ table: \"users\", on: \"orders.user_id.eq.users.id\" }, { table: \"products\", on: \"orders.product_id.eq.products.id\" }], columns: [\"orders.id\", \"users.name\", \"users.email\", \"products.title\", \"products.price\"]) }"
}

Mutation — insert rows

One mutation field is generated per table: insert<TableName>.

Signature

insertTableName(rows: [TableNameInput!]!): InsertTableNameResult

TableNameInput type

Each table gets a corresponding <TableName>Input input object. All fields are optional strings — include only the columns you want to set.

FieldTypeDescription
<column>StringAny column from the introspected table

InsertTableNameResult type

FieldTypeDescription
rowsInsertedIntNumber of inserted rows

Insert a single row

{
  "query": "mutation { insertUsers(rows: [{ name: \"Alice\", age: \"30\", email: \"[email protected]\" }]) { rowsInserted } }"
}

Insert multiple rows

{
  "query": "mutation { insertUsers(rows: [{ name: \"Alice\", age: \"30\" }, { name: \"Bob\", age: \"25\" }]) { rowsInserted } }"
}

Using variables

{
  "query": "mutation InsertUser($rows: [UsersInput!]!) { insertUsers(rows: $rows) { rowsInserted } }",
  "variables": {
    "rows": [
      { "name": "Alice", "email": "[email protected]", "age": "30" }
    ]
  },
  "operationName": "InsertUser"
}

Mutation — update rows

One mutation field is generated per table: update<TableName>.

Signature

updateTableName(data: TableNameInput!, filter: String): UpdateTableNameResult

Arguments

ArgumentTypeRequiredDescription
dataTableNameInput!YesColumns to set (omit any column you don't want to change)
filterStringNoWhich rows to update (see Filter Syntax)

Warning: omitting filter updates every row in the table.

UpdateTableNameResult type

FieldTypeDescription
rowsAffectedIntNumber of updated rows

Update a single row by ID

{
  "query": "mutation { updateUsers(data: { status: \"inactive\" }, filter: \"id.eq.42\") { rowsAffected } }"
}

Update multiple columns with a filter

{
  "query": "mutation { updateUsers(data: { status: \"suspended\", updated_at: \"2024-06-01\" }, filter: \"age.lt.18\") { rowsAffected } }"
}

Using variables

{
  "query": "mutation UpdateUser($data: UsersInput!, $filter: String) { updateUsers(data: $data, filter: $filter) { rowsAffected } }",
  "variables": {
    "data": { "name": "Alice Smith", "status": "active" },
    "filter": "id.eq.42"
  },
  "operationName": "UpdateUser"
}

Mutation — delete rows

One mutation field is generated per table: delete<TableName>.

Signature

deleteTableName(filter: String): DeleteTableNameResult

DeleteTableNameResult type

FieldTypeDescription
rowsAffectedIntNumber of deleted rows

Hard capped at 1000 rows per mutation.

Delete with filter

{
  "query": "mutation { deleteUsers(filter: \"status.eq.inactive\") { rowsAffected } }"
}

Delete with named mutation

{
  "query": "mutation CleanupSessions { deleteSessions(filter: \"expires_at.lt.2024-01-01\") { rowsAffected } }"
}

Variables

GraphQL variables let you parameterize queries without string interpolation.

{
  "query": "query GetUsers($filter: String, $limit: Int, $offset: Int) { users(filter: $filter, limit: $limit, offset: $offset) { id name email } }",
  "variables": {
    "filter": "age.gte.21",
    "limit": 20,
    "offset": 0
  },
  "operationName": "GetUsers"
}

Introspection

Use standard GraphQL introspection to discover the schema for your project — all table names, fields, and their types.

List all available query fields (tables)

{
  "query": "{ __schema { queryType { fields { name description args { name type { name kind ofType { name kind } } } } } } }"
}

Inspect a specific type

{
  "query": "{ __type(name: \"users\") { fields { name type { name kind } } } }"
}

Database Firewall Setup

For FastCRUD to connect to your database, your database firewall must allow inbound connections from FastCRUD's egress IPs. These are static IPs that don't change.

Getting the IPs

Via API

GET https://eu-fr-1.fastcrud.dev/network/egress-ips
{
  "region": "eu-fr-1",
  "egress_ips": ["51.x.x.x", "51.x.x.y"],
  "note": "Allow these IPs in your database firewall..."
}

Via Dashboard

Open any project → Configuration section. The egress IPs are displayed with a copy button.

PostgreSQL

pg_hba.conf (self-hosted)

# Add a line for each FastCRUD IP
# TYPE  DATABASE  USER      ADDRESS          METHOD
host    all       fastcrud  51.x.x.x/32      scram-sha-256
host    all       fastcrud  51.x.x.y/32      scram-sha-256

Reload after editing:

sudo systemctl reload postgresql

PostgreSQL with UFW (Ubuntu/Debian)

# Allow FastCRUD IPs to reach PostgreSQL port
sudo ufw allow from 51.x.x.x to any port 5432 proto tcp
sudo ufw allow from 51.x.x.y to any port 5432 proto tcp

PostgreSQL with iptables

sudo iptables -A INPUT -p tcp -s 51.x.x.x --dport 5432 -j ACCEPT
sudo iptables -A INPUT -p tcp -s 51.x.x.y --dport 5432 -j ACCEPT

# Save (Debian/Ubuntu)
sudo iptables-save | sudo tee /etc/iptables/rules.v4

MySQL

MySQL user creation with host restriction

-- Create a user that can only connect from FastCRUD IPs
CREATE USER 'fastcrud'@'51.x.x.x' IDENTIFIED BY 'your_password';
CREATE USER 'fastcrud'@'51.x.x.y' IDENTIFIED BY 'your_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON your_database.* TO 'fastcrud'@'51.x.x.x';
GRANT SELECT, INSERT, UPDATE, DELETE ON your_database.* TO 'fastcrud'@'51.x.x.y';
FLUSH PRIVILEGES;

Firewall (iptables)

sudo iptables -A INPUT -p tcp -s 51.x.x.x --dport 3306 -j ACCEPT
sudo iptables -A INPUT -p tcp -s 51.x.x.y --dport 3306 -j ACCEPT

SQL Server

Windows Firewall (PowerShell)

New-NetFirewallRule -DisplayName "FastCRUD Access" `
  -Direction Inbound `
  -RemoteAddress 51.x.x.x,51.x.x.y `
  -LocalPort 1433 `
  -Protocol TCP `
  -Action Allow

Linux (iptables)

sudo iptables -A INPUT -p tcp -s 51.x.x.x --dport 1433 -j ACCEPT
sudo iptables -A INPUT -p tcp -s 51.x.x.y --dport 1433 -j ACCEPT

MongoDB

mongod.conf — bind to all interfaces and use authentication:

net:
  bindIp: 0.0.0.0
  port: 27017
security:
  authorization: enabled

Then restrict access at the firewall level:

sudo iptables -A INPUT -p tcp -s 51.x.x.x --dport 27017 -j ACCEPT
sudo iptables -A INPUT -p tcp -s 51.x.x.y --dport 27017 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 27017 -j DROP

Oracle

sqlnet.ora — restrict by IP:

tcp.validnode_checking = YES
tcp.invited_nodes = (51.x.x.x, 51.x.x.y)

Or use firewall rules:

sudo iptables -A INPUT -p tcp -s 51.x.x.x --dport 1521 -j ACCEPT
sudo iptables -A INPUT -p tcp -s 51.x.x.y --dport 1521 -j ACCEPT

Cloud-hosted databases

AWS RDS / Aurora

  1. Go to RDS → your database → Security Group
  2. Edit Inbound Rules
  3. Add rule: Type = your DB type (PostgreSQL/MySQL/etc.), Source = 51.x.x.x/32
  4. Add another rule for each FastCRUD IP
  5. If your RDS is in a private subnet, you'll also need the VPC to be reachable from the internet (public subnet + internet gateway) or use VPC peering

AWS EC2

Edit the instance's Security Group inbound rules to allow the FastCRUD IPs on your database port.

Google Cloud SQL

  1. Go to Cloud SQL → your instance → Connections → Networking
  2. Under Authorized networks, add each FastCRUD IP with a /32 suffix
  3. Click Save

Azure Database

  1. Go to your database → Networking / Firewall rules
  2. Add a rule for each FastCRUD IP (start IP = end IP)
  3. Save

Neon (Serverless Postgres)

Neon allows all IPs by default. If you've enabled IP Allow List:

  1. Go to Project Settings → IP Allow
  2. Add each FastCRUD egress IP

PlanetScale (MySQL)

PlanetScale allows connections from any IP by default when using their connection strings. No firewall changes needed.

MongoDB Atlas

  1. Go to Network Access in your Atlas project
  2. Click Add IP Address
  3. Add each FastCRUD egress IP
  4. Click Confirm

Supabase (Postgres)

Supabase allows all connections by default on the connection pooler. If you've enabled network restrictions, add the FastCRUD IPs to the allowlist in your project's database settings.

Important: Replace 51.x.x.x and 51.x.x.y with the actual IPs from the /network/egress-ips endpoint or the dashboard. These IPs are static and won't change without notice.


IP Access Rules

IP access rules let you restrict which IP addresses can make API requests to your project. When enabled, only requests originating from an allowed IP or CIDR range will be accepted — all others receive 403 Forbidden.

How it works

  • No rules configured — all IPs are allowed (default, open access).
  • One or more rules configured — only listed IPs/CIDRs are allowed. Everything else is blocked.
  • Rules apply to all /crud/*, /graphql, and /query/* endpoints for the project.
  • Authentication endpoints (/authenticate/crud/:accessKeyID) are not affected.

Managing rules in the dashboard

Open your project in the FastCRUD dashboard, expand Security & Access, and scroll to the IP Access Rules section. Add individual IPs or CIDR ranges:

FormatExampleWhat it matches
Single IP203.0.113.42Exactly that IP
CIDR range10.0.0.0/2410.0.0.0 – 10.0.0.255 (256 addresses)
CIDR range192.168.1.0/16192.168.0.0 – 192.168.255.255

Finding your server IP

Before adding a rule, determine the public IP your application makes outbound requests from.

Linux / macOS

curl -s https://ifconfig.me

Windows (PowerShell)

(Invoke-WebRequest -Uri https://ifconfig.me -UseBasicParsing).Content

From within your app (programmatic)

# If your app runs behind a NAT or load balancer, the outbound IP
# may differ from your machine's local IP. Always check from the
# same environment that will call the FastCRUD API.
curl -s https://ifconfig.me

Firewall examples

If you also want to lock down your own server so only FastCRUD's response traffic is allowed, or you want to restrict outbound access, here are common firewall configurations.

These examples assume your server calls the FastCRUD API at eu-fr-1.fastcrud.dev. Replace the IP with the resolved address or your own requirements.

iptables (most Linux distributions)

# Allow outbound HTTPS to FastCRUD API
sudo iptables -A OUTPUT -p tcp -d eu-fr-1.fastcrud.dev --dport 443 -j ACCEPT

# Allow established/related return traffic
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Save rules (Debian/Ubuntu)
sudo iptables-save | sudo tee /etc/iptables/rules.v4

# Save rules (RHEL/CentOS)
sudo service iptables save

ufw (Ubuntu / Debian)

# Allow outbound HTTPS traffic (ufw allows all outbound by default,
# but if you've set a restrictive outbound policy):
sudo ufw allow out to any port 443 proto tcp

# If you want to restrict to a specific IP:
FASTCRUD_IP=$(dig +short eu-fr-1.fastcrud.dev | head -1)
sudo ufw allow out to $FASTCRUD_IP port 443 proto tcp

# Check status
sudo ufw status verbose

firewalld (RHEL / CentOS / Fedora)

# Allow outbound HTTPS
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

# Or allow a specific IP
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" destination address="<FASTCRUD_IP>" port port="443" protocol="tcp" accept'
sudo firewall-cmd --reload

nftables (modern Linux)

# Add to your nftables ruleset
sudo nft add rule inet filter output ip daddr <FASTCRUD_IP> tcp dport 443 accept

# Make persistent — edit /etc/nftables.conf and add the rule, then:
sudo systemctl restart nftables

Windows Firewall (PowerShell)

# Allow outbound HTTPS to FastCRUD
$ip = (Resolve-DnsName eu-fr-1.fastcrud.dev -Type A).IPAddress
New-NetFirewallRule -DisplayName "Allow FastCRUD API" `
  -Direction Outbound `
  -RemoteAddress $ip `
  -RemotePort 443 `
  -Protocol TCP `
  -Action Allow

Windows Firewall (netsh)

:: Resolve the IP first, then:
netsh advfirewall firewall add rule name="Allow FastCRUD API" ^
  dir=out action=allow protocol=tcp remoteport=443 ^
  remoteip=<FASTCRUD_IP>

Tip: FastCRUD's API IP may change if it's behind a load balancer. If you need a stable set of IPs, contact support. For most setups, allowing outbound HTTPS to any destination is sufficient — the IP access rules on the FastCRUD side handle inbound restriction.


Limits & Defaults

SettingValueNotes
Default limit1000Applied when limit is omitted
Maximum limit1000Values above 1000 are silently capped
Default offset0
Max delete rows1000Hard cap per request
Insert rowsUnlimitedConstrained only by database and request size
Update scopeAll rowsNo row cap — use a filter to restrict scope
Schema cache TTL10 minGraphQL schema is rebuilt after expiry
Column cache TTL10 minTable structure is cached in Redis

Error Responses

REST errors

{ "error": "description of the problem" }

GraphQL errors

GraphQL errors appear in the errors array alongside any partial data:

{
  "data": null,
  "errors": [
    { "message": "column 'foo' does not exist in metadata - maybe database has not been introspected?" }
  ]
}

Common errors

MessageCause
invalid project idMissing or expired Bearer token
failed to create connection to database / project does not existNo database connection stored for this project
no tables found for project - maybe database has not been introspected?Database introspection hasn't run yet
column 'x' does not exist in metadataColumn name doesn't match introspected schema
no rows providedINSERT body was an empty array
no data provided for updateUPDATE body was an empty object
body must be a JSON object or array of objectsINSERT body could not be parsed
body must be a JSON objectUPDATE body could not be parsed
invalid filter format: ...Filter token is not in col.op.value format
invalid operator: ...Operator is not one of the supported values
invalid join 'on' format: ...Join condition is not in t1.c1.op.t2.c2 format
invalid join type: ...Join type is not INNER, LEFT, or RIGHT

CLI Code Generator

The FastCRUD CLI reads your project's introspected schema and generates typed models and a ready-to-use CRUD client in Go, C#, TypeScript, or Java.

fastcrud-cli --key YOUR_ACCESS_KEY_ID --lang typescript --output ./src/api

See the full CLI documentation for installation, usage, and examples in every supported language.