An open-source backend platform for building full-stack apps on Cloudflare.
Fine provides database access, authentication, file storage, and row-level security—all deployable to the edge with Cloudflare Workers, D1, and R2. Originally built to power Fine.dev, it's now available for anyone to self-host, extend, and contribute to.
- 🔐 Authentication – Session-based auth via email/password or magic links
- 🥮 D1 Database API – RESTful access with full CRUD support
- 📆 R2 File Storage – Entity-linked uploads, downloads, and permissions
- 🔐 Row-Level Security (RLS) – Policy-based access per user
- 🔐 Secrets Management – Secure config binding in Workers
- 🤖 AI Assistant Integration – Bring LLMs into your workflows (optional)
The simplest way to get started is by generating a project at https://fine.dev.
- Configure Cloudflare Workers
- Set up D1 Database
- Create R2 Bucket
- Deploy the worker
- Set up environment variables:
-
DB
: D1 Database binding -
STORAGE_BUCKET
: R2 Bucket binding for file storage -
BYPASS_AUTH
: Set to true to bypass authentication (development only) -
VERSION
: API version information
- Run:
npm install
npm run dev
Before first run:
npx wrangler d1 execute BAAS_DATABASE_NAME --local --file=./schema.sql
To deploy:
npm run deploy
⚠️ Fine is currently in alpha. CLI and self-hosting workflows are actively improving.
Built for builders. Powered by Cloudflare. Fully yours to self-host.
The Storage API allows clients to upload, download, and manage files, with access control tied to database entity permissions.
Files are stored in Cloudflare R2 and linked to database entities. Access permissions are inherited from the database's Row-Level Security (RLS) policies - if a user has access to an entity, they can access its associated files.
All files are linked to database entities. To list files associated with any entity, you should query the database table directly. This ensures the database remains the single source of truth for file references.
Files are stored in R2 with the following path structure:
{table}/{id}/{field}/{filename}
For example, a user's profile picture might be stored at:
users/user123/avatar/profile.jpg
POST /storage/upload
Content-Type: multipart/form-data
Form Data:
- file: The file to upload
- entity: JSON object with {table, id, field}
- metadata: (optional) JSON object with custom metadata
GET /storage/download/:table/:id/:field/:filename
DELETE /storage/delete/:table/:id/:field/:filename
Upload a profile picture:
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('entity', JSON.stringify({
table: 'users',
id: 'user123',
field: 'avatar'
}));
fetch('/storage/upload', {
method: 'POST',
body: formData,
headers: {
'Authorization': 'Bearer your-token'
}
});
Display a profile picture:
<img src="/storage/download/users/user123/avatar/profile.jpg" alt="Profile Picture">
Listing user profile pictures:
// Query the database to find file references
fetch('/db/tables/users?select=id,avatar')
.then(response => response.json())
.then(data => {
// Process the file references from the database
const users = data.data;
users.forEach(user => {
if (user.avatar) {
// Display avatar using the download endpoint
const avatarUrl = `/storage/download/users/${user.id}/avatar/${user.avatar}`;
// Use the URL as needed
}
});
});
File operations inherit RLS policies from the referenced entity:
- To upload/update a file: User needs UPDATE permission on the entity
- To download files: User needs SELECT permission on the entity
- To delete a file: User needs DELETE permission on the entity
Before first run, you need to migrate the database:
npx wrangler d1 execute BAAS_DATABASE_NAME --local --file=./schema.sql
This API implements Row Level Security, allowing you to define access policies at the row level. RLS restricts which rows can be retrieved by normal database operations based on user identity.
- Each database operation (SELECT/INSERT/UPDATE/DELETE) is filtered through security policies.
- Policies define conditions for when operations are allowed.
- Two types of security checks:
-
using_clause
: Filters which rows users can SELECT, UPDATE, or DELETE -
withcheck_clause
: Filters which rows users can INSERT or UPDATE
-
Policies are stored in the _policies
table. Here's how to create a policy:
-- Example: Allow users to see only their own todos
INSERT INTO _policies (table_name, action, using_clause)
VALUES ('todos', 'select', 'user_id = $$CURRENT_USER$$');
-- Example: Allow users to insert only todos they own
INSERT INTO _policies (table_name, action, withcheck_clause)
VALUES ('todos', 'insert', 'user_id = $$CURRENT_USER$$');
-- Example: Allow users to update their own todos
INSERT INTO _policies (table_name, action, using_clause, withcheck_clause)
VALUES (
'todos',
'update',
'user_id = $$CURRENT_USER$$',
'user_id = $$CURRENT_USER$$'
);
-- Example: Allow users to delete their own todos
INSERT INTO _policies (table_name, action, using_clause)
VALUES ('todos', 'delete', 'user_id = $$CURRENT_USER$$');
Special variables in policy expressions:
-
$$CURRENT_USER$$
: The ID of the authenticated user -
$$CURRENT_ROLE$$
: The role of the authenticated user (if available)
Here are examples of how to use cURL to test the database endpoints:
# Get all todos
curl -X GET "http://localhost:8787/db/tables/todos"
# Filter by field
curl -X GET "http://localhost:8787/db/tables/todos?completed=false"
# Advanced filtering, limit, and ordering
curl -X GET "http://localhost:8787/db/tables/todos?title.like=important&limit=10&order=created_at%20DESC"
# Get todo by ID
curl -X GET "http://localhost:8787/db/tables/todos/123"
# Create a new todo
curl -X POST "http://localhost:8787/db/tables/todos" \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries", "user_id": "user123", "completed": false}'
# Update todo by ID
curl -X PATCH "http://localhost:8787/db/tables/todos/123" \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Update with a WHERE condition
curl -X PATCH "http://localhost:8787/db/tables/todos" \
-H "Content-Type: application/json" \
-d '{
"data": {"completed": true},
"where": {"user_id": "user123"}
}'
# Delete todo by ID
curl -X DELETE "http://localhost:8787/db/tables/todos/123"
# Delete with a WHERE condition
curl -X DELETE "http://localhost:8787/db/tables/todos" \
-H "Content-Type: application/json" \
-d '{
"where": {"completed": true}
}'
# Greater than
curl -X GET "http://localhost:8787/db/tables/todos?priority.gt=3"
# Less than or equal to
curl -X GET "http://localhost:8787/db/tables/todos?created_at.lte=2023-12-31"
# Not equal
curl -X GET "http://localhost:8787/db/tables/todos?status.neq=cancelled"
# LIKE pattern matching
curl -X GET "http://localhost:8787/db/tables/todos?title.like=meeting"