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
- Supported Databases
- Authentication
- Filter Syntax
- REST API
- GraphQL API
- Database Firewall Setup
- IP Access Rules
- Limits & Defaults
- Error Responses
API Regions
FastCRUD CRUD API endpoints are region-specific. Use the hostname closest to your database for the lowest latency.
| Region | Hostname | Location |
|---|---|---|
| EU France 1 | eu-fr-1.fastcrud.dev | Paris, 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.
| dbType | Database | JOIN support | Notes |
|---|---|---|---|
0 | PostgreSQL | Yes | Default; uses pgx driver |
1 | Oracle | Yes | Tested with Oracle 12c+ |
2 | MySQL | Yes | Uses ? placeholders |
3 | SQL Server | Yes | Uses @p1 placeholders |
4 | MongoDB | No | Collections 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.
| Database | Internal format |
|---|---|
| PostgreSQL | postgresql://user:pass@host:port/db?sslmode=require |
| MySQL | user:pass@tcp(host:port)/db |
| SQL Server | sqlserver://user:pass@host:port?database=db |
| Oracle | oracle://user:pass@host:port/service |
| MongoDB | mongodb://user:pass@host:port/db |
For MongoDB, the
databasefield 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
| Parameter | In | Type | Description |
|---|---|---|---|
| accessKeyID | path | UUID | Your 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
| Operator | SQL equivalent | Example |
|---|---|---|
eq | = | status.eq.active |
neq | != | status.neq.deleted |
gt | > | age.gt.18 |
gte | >= | age.gte.18 |
lt | < | price.lt.100 |
lte | <= | price.lte.100 |
like | LIKE | name.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
| Parameter | Type | Description |
|---|---|---|
| table | string | Table name |
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| filter | string | — | Filter expression (see Filter Syntax) |
| limit | int | 1000 | Max rows to return (capped at 1000) |
| offset | int | 0 | Number 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
| Parameter | Type | Description |
|---|---|---|
| table | string | Table name |
| columns | string | Comma-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
| Parameter | Type | Description |
|---|---|---|
| table | string | Table 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
| Parameter | Type | Description |
|---|---|---|
| table | string | Table name |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| filter | string | No | Filter expression |
Warning: omitting
filterupdates 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
| Parameter | Type | Description |
|---|---|---|
| table | string | Table name |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| filter | string | No | Filter expression |
Warning: omitting
filterdeletes 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
| Argument | Type | Default | Description |
|---|---|---|---|
| filter | String | — | Filter expression (see Filter Syntax) |
| columns | [String] | all | Columns to return; omit to return all |
| limit | Int | 1000 | Max rows (capped at 1000) |
| offset | Int | 0 | Rows 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
| Field | Type | Required | Description |
|---|---|---|---|
| table | String | Yes | The table to join |
| on | String | Yes | Join condition: table1.col1.operator.table2.col2 |
| type | String | No | INNER (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.
| Field | Type | Description |
|---|---|---|
<column> | String | Any column from the introspected table |
InsertTableNameResult type
| Field | Type | Description |
|---|---|---|
| rowsInserted | Int | Number 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
| Argument | Type | Required | Description |
|---|---|---|---|
| data | TableNameInput! | Yes | Columns to set (omit any column you don't want to change) |
| filter | String | No | Which rows to update (see Filter Syntax) |
Warning: omitting
filterupdates every row in the table.
UpdateTableNameResult type
| Field | Type | Description |
|---|---|---|
| rowsAffected | Int | Number 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
| Field | Type | Description |
|---|---|---|
| rowsAffected | Int | Number 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
- Go to RDS → your database → Security Group
- Edit Inbound Rules
- Add rule: Type = your DB type (PostgreSQL/MySQL/etc.), Source =
51.x.x.x/32 - Add another rule for each FastCRUD IP
- 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
- Go to Cloud SQL → your instance → Connections → Networking
- Under Authorized networks, add each FastCRUD IP with a
/32suffix - Click Save
Azure Database
- Go to your database → Networking / Firewall rules
- Add a rule for each FastCRUD IP (start IP = end IP)
- Save
Neon (Serverless Postgres)
Neon allows all IPs by default. If you've enabled IP Allow List:
- Go to Project Settings → IP Allow
- 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
- Go to Network Access in your Atlas project
- Click Add IP Address
- Add each FastCRUD egress IP
- 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.xand51.x.x.ywith the actual IPs from the/network/egress-ipsendpoint 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:
| Format | Example | What it matches |
|---|---|---|
| Single IP | 203.0.113.42 | Exactly that IP |
| CIDR range | 10.0.0.0/24 | 10.0.0.0 – 10.0.0.255 (256 addresses) |
| CIDR range | 192.168.1.0/16 | 192.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
| Setting | Value | Notes |
|---|---|---|
| Default limit | 1000 | Applied when limit is omitted |
| Maximum limit | 1000 | Values above 1000 are silently capped |
| Default offset | 0 | |
| Max delete rows | 1000 | Hard cap per request |
| Insert rows | Unlimited | Constrained only by database and request size |
| Update scope | All rows | No row cap — use a filter to restrict scope |
| Schema cache TTL | 10 min | GraphQL schema is rebuilt after expiry |
| Column cache TTL | 10 min | Table 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
| Message | Cause |
|---|---|
invalid project id | Missing or expired Bearer token |
failed to create connection to database / project does not exist | No 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 metadata | Column name doesn't match introspected schema |
no rows provided | INSERT body was an empty array |
no data provided for update | UPDATE body was an empty object |
body must be a JSON object or array of objects | INSERT body could not be parsed |
body must be a JSON object | UPDATE 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.