# Cufflink
Deploy CRUD microservices in seconds. Define your data model in Rust, run `cufflink deploy`, and get a full REST API with PostgreSQL persistence, automatic schema migrations, NATS events, and multi-tenant isolation.
## Architecture
```
┌─────────────┐ ┌──────────────┐ ┌───────────┐
│ Cufflink │────>│ Platform │────>│ PostgreSQL│
│ CLI │ │ (Axum) │ │ │
└─────────────┘ └──────┬───────┘ └───────────┘
│
┌─────────────┐ ┌──────┴───────┐ ┌───────────┐
│ Web Admin │────>│ Dynamic │────>│ NATS │
│ (Next.js) │ │ CRUD Router │ │ JetStream │
└─────────────┘ └──────────────┘ └───────────┘
```
**SDK** — `#[derive(Table)]` proc macros generate JSON manifests from Rust structs
**CLI** — `cufflink deploy` captures the manifest and POSTs it to the platform
**Platform** — Applies schema migrations, registers CRUD routes, publishes events
**Web Admin** — Dashboard for browsing services, data, schemas, and deployments
## Quick Start
### Prerequisites
- Rust 1.70+
- Docker & Docker Compose
### 1. Start the platform
```bash
docker compose up -d
```
This starts PostgreSQL, NATS, Keycloak, the Cufflink platform, and the web admin.
- Platform API: http://localhost:8080
- Web Admin: http://localhost:3000
- NATS Monitoring: http://localhost:8222
### 2. Define a service
```rust
// examples/todo-service/src/main.rs
use cufflink::prelude::*;
#[derive(Table, Serialize, Deserialize, Clone)]
#[table(name = "todos")]
pub struct Todo {
#[key] pub id: Uuid,
pub title: String,
pub completed: bool,
#[timestamp] pub created_at: DateTime<Utc>,
#[timestamp] pub updated_at: DateTime<Utc>,
}
cufflink::service! {
name: "todo-service",
tables: [Todo],
}
```
For services with authentication and permissions:
```rust
#[derive(Table, Serialize, Deserialize, Clone)]
#[table(name = "todos", auth_required, permission_area = "todos")]
pub struct Todo { /* ... */ }
cufflink::service! {
name: "todo-service",
tables: [Todo],
authorization: [
areas: [("todos", ["create", "view", "edit", "delete"])],
default_roles: [
("admin", "Full access", ["todos:*"]),
("viewer", "Read-only", ["todos:view"]),
],
],
}
```
### 3. Deploy
```bash
cd examples/todo-service
cargo run -p cufflink -- deploy
```
### 4. Use the API
```bash
# Create
curl -X POST http://localhost:8080/svc/default/todo-service/todos \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries", "completed": false}'
# List (with filtering, sorting, search, pagination)
curl "http://localhost:8080/svc/default/todo-service/todos?sort=-created_at&per_page=10"
# Search
curl "http://localhost:8080/svc/default/todo-service/todos?search=groceries"
# Filter
curl --get http://localhost:8080/svc/default/todo-service/todos \
--data-urlencode 'filter=[{"n":"completed","f":"=","v":"false"}]'
# Get by ID
curl http://localhost:8080/svc/default/todo-service/todos/{id}
# Update
curl -X PUT http://localhost:8080/svc/default/todo-service/todos/{id} \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Delete
curl -X DELETE http://localhost:8080/svc/default/todo-service/todos/{id}
```
## SDK Reference
### Table Attributes
| `#[table(name = "...")]` | SQL table name (required) |
| `#[table(auth_required)]` | Require authentication for CRUD access |
| `#[table(permission_area = "...")]` | Enable CRUD permission enforcement (e.g. `"staff"` checks `staff:view`, `staff:create`, etc.) |
| `#[key]` | Primary key (auto-generated UUID) |
| `#[timestamp]` | Auto-generated timestamp (DEFAULT now()) |
| `#[default("value")]` | Default value |
| `#[references("table.column")]` | Foreign key reference |
| `#[on_delete("cascade")]` | FK delete action: cascade, set_null, restrict, no_action |
| `#[hooks(before_create = "...")]` | CRUD lifecycle hooks — call WASM handlers before/after operations |
### Supported Types
| `Uuid` | UUID |
| `String` | TEXT |
| `bool` | BOOLEAN |
| `i32` | INTEGER |
| `i64` | BIGINT |
| `f32` | REAL |
| `f64` | DOUBLE PRECISION |
| `DateTime<Utc>` | TIMESTAMPTZ |
| `NaiveDate` | DATE |
| `Value` | JSONB |
| `Option<T>` | Nullable version of T |
### Foreign Keys
```rust
#[derive(Table, Serialize, Deserialize, Clone)]
#[table(name = "comments")]
pub struct Comment {
#[key] pub id: Uuid,
#[references("posts.id")]
#[on_delete("cascade")]
pub post_id: Uuid,
pub body: String,
}
```
## CLI Reference
| `cufflink init <name>` | Scaffold a new service project |
| `cufflink deploy` | Deploy the service in the current directory |
| `cufflink deploy --allow-destructive` | Allow DROP COLUMN / DROP TABLE |
| `cufflink rollback` | Rollback to previous version |
| `cufflink rollback --version N` | Rollback to specific version |
| `cufflink status` | Show service status and endpoints |
| `cufflink services` | List all deployed services |
| `cufflink logs` | Stream service logs |
| `cufflink openapi` | Print OpenAPI spec |
| `cufflink openapi --output spec.json` | Save OpenAPI spec to file |
| `cufflink generate-client` | Generate TypeScript client |
| `cufflink login` | Authenticate with the platform |
### Configuration
| `CUFFLINK_API_URL` | `http://localhost:8080` | Platform API URL |
| `CUFFLINK_TENANT` | `default` | Tenant slug |
| `CUFFLINK_KEYCLOAK_URL` | `http://localhost:8180` | Keycloak URL |
| `CUFFLINK_CLIENT_ID` | `cufflink-cli` | Keycloak client ID |
## API Endpoints
### Management API
| GET | `/health` | Health check |
| POST | `/api/auth/bootstrap` | Create first API key (no auth) |
| POST | `/api/auth/api-keys` | Create API key (requires auth) |
| GET | `/api/auth/me` | Get authenticated user info |
| GET | `/api/services` | List all services |
| GET | `/api/services/{id}` | Get service details |
| DELETE | `/api/services/{id}` | Delete a service |
| POST | `/api/services/deploy` | Deploy a service |
| GET | `/api/services/{id}/deployments` | Deployment history |
| POST | `/api/services/{id}/rollback` | Rollback deployment |
| GET | `/api/services/{id}/openapi.json` | Auto-generated OpenAPI spec |
| POST | `/api/services/{id}/wasm` | Upload WASM module |
| GET | `/api/services/{id}/wasm/status` | WASM runtime status |
### Dynamic CRUD Routes
| GET | `/svc/{tenant}/{service}/{table}` | List records |
| POST | `/svc/{tenant}/{service}/{table}` | Create record |
| GET | `/svc/{tenant}/{service}/{table}/{id}` | Get record |
| PUT | `/svc/{tenant}/{service}/{table}/{id}` | Update record |
| DELETE | `/svc/{tenant}/{service}/{table}/{id}` | Delete record |
### Query Parameters
| `page` | `?page=2` | Page number |
| `per_page` | `?per_page=25` | Items per page (max 200) |
| `sort` | `?sort=-created_at,title` | Sort (prefix `-` for DESC) |
| `search` | `?search=hello` | Full-text search across text columns |
| `filter` | `?filter=[{"n":"status","f":"=","v":"active"}]` | JSON filter array |
| `select` | `?select=id,title` | Column selection |
### Filter Operators
`=`, `!=`, `>`, `<`, `>=`, `<=`, `LIKE`, `ILIKE`, `IN`, `IS NULL`, `IS NOT NULL`
## Authentication
### Bootstrap (first-time setup)
```bash
curl -X POST http://localhost:8080/api/auth/bootstrap \
-H "Content-Type: application/json" \
-d '{"tenant_name": "My Org", "tenant_slug": "my-org"}'
```
### Using API Keys
```bash
curl -H "Authorization: ApiKey ck_..." http://localhost:8080/api/auth/me
```
### CLI Login
```bash
cufflink login
```
## Project Structure
```
cufflink/
├── platform/ # Axum-based platform server
├── cli/ # CLI tool (cufflink binary)
├── sdk/ # User-facing SDK crate
├── sdk-macros/ # Proc macros (#[derive(Table)], service!{})
├── types/ # Shared types (ServiceManifest, etc.)
├── web/ # Next.js web admin dashboard
├── examples/
│ ├── todo-service/ # Simple single-table example
│ └── blog-service/ # Multi-table example with FK
├── docker-compose.yml
└── Dockerfile.platform
```
## Development
```bash
# Build everything
cargo build --workspace
# Run tests
cargo test --workspace
# Run platform locally (requires Docker services)
docker compose up -d postgres nats keycloak
cargo run -p cufflink-platform
# Run web admin locally
cd web && npm install && npm run dev
```
## License
MIT