Files
tessera/docs/architecture.md
2026-06-07 21:09:26 +02:00

311 lines
14 KiB
Markdown

# Tessera Architecture
## Principle
Tessera is RT's paradigm rebuilt: a ticketing system that makes zero assumptions about your workflow. It provides composable primitives — scrips, custom fields, lifecycles, transactions — and you build your process on top.
Everything is a transaction. Everything is queryable. Everything is automatable.
## Philosophy
- **Config > Code**: Scrips are defined in a web UI, not in source files. The scrip runtime is sandboxed, the conditions and actions are configurable from data.
- **Prepare/Commit**: Every state change goes through a two-phase dispatch that can be dry-run. No action executes without the user knowing what will happen.
- **SystemUser**: Automation runs elevated. The system acts on behalf of the configuration, not the triggering user's permissions.
- **Single Binary**: The core (Rust) ships as one binary. Integration plugins (Go) are sidecar processes over gRPC.
- **PostgreSQL**: No ORM. Raw SQL with compile-time query building. Query builder compiles to SQL, not to an ORM abstraction.
## MVP Scope — "ScripFoundry"
The MVP is the scrip runtime + enough scaffolding to exercise it. This is the wedge: nobody else has a configurable event-driven automation engine with prepare/commit and a web UI for defining rules.
### What's In
1. **Ticket model** — id, subject, queue_id, status, owner_id, creator_id, created_at, updated_at
2. **Queue model** — id, name, description, lifecycle_id
3. **Transaction model** — id, ticket_id, transaction_type, field, old_value, new_value, creator_id, created_at
4. **Lifecycle state machine** — per-queue status definitions, transitions, transition-gating rights. Hardcoded "default" lifecycle for MVP.
5. **Scrip engine** (the crown jewel):
- Define conditions (OnCreate, OnStatusChange, OnResolve, UserDefined)
- Define actions (SendEmail, SetCustomField, Webhook)
- Define templates (variable substitution: `{$ticket.id}`, `{$ticket.subject}`, etc.)
- Prepare/Commit dispatch with sort ordering
- TransactionCreate and TransactionBatch stages
- Dry-run mode: /api/tickets/:id/preview returns "what will happen if I apply this update"
6. **Custom Fields (MVP subset)**:
- Freeform text, Select single, Select multiple
- Attached to tickets
- Queryable
- Appear in variable substitution
7. **REST API** — CRUD for tickets, queues, transactions, scrips, custom fields. The UI is a separate concern.
8. **Email actions** — SMTP send via scrip action. Template-driven subject/body.
9. **Webhook actions** — HTTP POST on scrip trigger. Template-driven URL/body/headers.
### What's Out (Post-MVP)
- User management (hardcode admin user)
- Authentication/authorization (trust boundary not in MVP)
- Full ACL system
- Transaction query builder UI (API-only)
- Multi-tenancy
- Migration/import tools
- Attachments
- Real email ingestion (only outbound for MVP)
- Notifications beyond email and webhooks (no Slack, SMS, etc.)
- Admin web UI (API-driven, build later)
- Custom scrip code execution (UserDefined is config-based, not code-based in MVP)
## Architecture
### Component Diagram
```
┌─────────────────────────────────────────────────┐
│ tessera-core (Rust) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ HTTP API │ │ Scrip │ │ Lifecycle │ │
│ │ (axum) │ │ Engine │ │ State Machine │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ┌────┴────────────┴───────────────┴──────────┐ │
│ │ Query Builder │ │
│ │ (SQL generation from typed filter ASTs) │ │
│ └────────────────────┬───────────────────────┘ │
│ │ │
│ ┌────────────────────┴───────────────────────┐ │
│ │ PostgreSQL (sqlx) │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ gRPC Server (tonic) │ │
│ └────────────────┬───────────────────────────┘ │
└───────────────────┼────────────────────────────────┘
┌───────────┼───────────┐
│ │ │
┌───────┴───┐ ┌─────┴─────┐ ┌──┴──────────┐
│ Email │ │ Webhook │ │ Custom ... │
│ Connector│ │ Connector│ │ Connectors │
│ (Go) │ │ (Go) │ │ (Go) │
└───────────┘ └───────────┘ └──────────────┘
```
### Technology Decisions
| Layer | Technology | Rationale |
|-------|-----------|-----------|
| HTTP framework | axum | Ergonomic, tower-based middleware, best-in-class Rust HTTP |
| Database | PostgreSQL 15+ | Transactional DDL, CTEs, JSONB, full-text search |
| SQL driver | sqlx | Compile-time checked queries, async, migrations |
| gRPC | tonic/prost | Well-typed contracts between Rust core and Go integrations |
| Scrip sandbox | wasmtime | Execute user-defined scrip conditions/actions in WASM sandbox |
| Templating | Tera | Django-like syntax, variable interpolation |
| Serialization | serde + serde_json | Standard |
| Logging | tracing | Structured, async-aware |
| Config | figment | Hierarchical config (file + env + defaults) |
| Integration runtime | Go 1.22+ | Single binary, excellent gRPC support, ops ecosystem |
### Data Model (Core Tables)
```sql
-- Tickets
CREATE TABLE tickets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
subject TEXT NOT NULL,
queue_id UUID NOT NULL REFERENCES queues(id),
status TEXT NOT NULL,
owner_id UUID REFERENCES users(id),
creator_id UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
resolved_at TIMESTAMPTZ
);
-- Queues
CREATE TABLE queues (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
description TEXT,
lifecycle_id UUID REFERENCES lifecycles(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Lifecycles
CREATE TABLE lifecycles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
definition JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Transactions (the universal event log)
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
transaction_type TEXT NOT NULL, -- 'Create', 'StatusChange', 'Correspond', 'Comment', 'CustomField', 'EmailRecord', etc.
field TEXT,
old_value TEXT,
new_value TEXT,
data JSONB,
creator_id UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Scrips
CREATE TABLE scrips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
queue_id UUID REFERENCES queues(id), -- NULL = global
name TEXT NOT NULL,
description TEXT,
condition_type TEXT NOT NULL, -- 'OnCreate', 'OnStatusChange', etc.
condition_config JSONB NOT NULL DEFAULT '{}',
action_type TEXT NOT NULL, -- 'SendEmail', 'SetCustomField', 'Webhook'
action_config JSONB NOT NULL DEFAULT '{}',
template_id UUID REFERENCES templates(id),
stage TEXT NOT NULL DEFAULT 'TransactionCreate', -- 'TransactionCreate' or 'TransactionBatch'
sort_order INT NOT NULL DEFAULT 0,
disabled BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Templates
CREATE TABLE templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
queue_id UUID REFERENCES queues(id), -- NULL = global
subject_template TEXT NOT NULL,
body_template TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(name, queue_id)
);
-- Custom Fields
CREATE TABLE custom_fields (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
field_type TEXT NOT NULL, -- 'Freeform', 'SelectSingle', 'SelectMulti'
values JSONB, -- For select types: ["opt1", "opt2"]
max_values INT NOT NULL DEFAULT 1, -- 0 = unlimited
pattern TEXT, -- Validation regex
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Custom Field <-> Queue attachment
CREATE TABLE queue_custom_fields (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
queue_id UUID REFERENCES queues(id),
custom_field_id UUID REFERENCES custom_fields(id) ON DELETE CASCADE,
sort_order INT NOT NULL DEFAULT 0,
UNIQUE(queue_id, custom_field_id)
);
-- Custom Field Values (on tickets)
CREATE TABLE custom_field_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
custom_field_id UUID NOT NULL REFERENCES custom_fields(id) ON DELETE CASCADE,
ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
value TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(custom_field_id, ticket_id, value)
);
-- Users (MVP: minimal)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
email TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```
### API Design (MVP Endpoints)
```
GET /api/health
GET /api/tickets
POST /api/tickets
GET /api/tickets/:id
PATCH /api/tickets/:id
POST /api/tickets/:id/preview -- Dry-run: returns scrips that would fire
GET /api/tickets/:id/transactions
POST /api/tickets/:id/comment -- Add comment (creates transaction)
GET /api/queues
POST /api/queues
GET /api/scrips
POST /api/scrips
GET /api/scrips/:id
PATCH /api/scrips/:id
GET /api/custom-fields
POST /api/custom-fields
GET /api/lifecycles
POST /api/lifecycles
POST /api/tickets/search -- TicketSQL query body
```
### Scrip Engine Flow
```
1. Ticket update request arrives → axum handler
2. Validate transition (lifecycle state machine)
3. Apply update → creates Transaction record(s)
4. Scrip Engine triggered:
a. Load matching scrips (global + queue-specific)
b. Filter by ApplicableTransTypes matching transaction type
c. Sort by sort_order
d. PREPARE PHASE: For each scrip:
- Evaluate condition (no side effects)
- If condition matches, build action payload with template substitution
- Push to prepared_scrips
e. COMMIT PHASE: For each prepared scrip:
- Execute action (send email, fire webhook, set CF, etc.)
- Create EmailRecord or WebhookDelivery transaction on success
5. Return response: { ticket, transactions, scrips_fired: [...] }
```
### Project Layout
```
tessera/
├── Cargo.toml
├── README.md
├── docs/
│ ├── architecture.md (this file)
│ └── rt-analysis.md (RT deep-dive reference)
├── crates/
│ ├── tessera-core/ Main library: models, scrip engine, lifecycle
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs
│ │ ├── models/ ticket, queue, transaction, scrip, etc.
│ │ ├── scrip/ engine, conditions, actions, templates
│ │ ├── lifecycle/ state machine, transition validation
│ │ ├── query/ query builder, TicketSQL AST
│ │ └── db/ sqlx queries, migrations
│ ├── tessera-api/ HTTP API server (axum)
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── main.rs
│ │ ├── routes/ ticket, queue, scrip, lifecycle, search
│ │ └── middleware/ logging, error handling
│ └── tessera-grpc/ gRPC server for integration connectors
│ ├── Cargo.toml
│ └── src/
├── proto/ Protobuf definitions
│ └── tessera/
│ └── v1/
│ ├── tickets.proto
│ ├── scrips.proto
│ └── actions.proto
├── integrations/ Go integration connectors
│ ├── go.mod
│ ├── cmd/
│ │ ├── email-connector/
│ │ └── webhook-connector/
│ └── pkg/
│ └── tessera/ Generated gRPC client
├── migrations/ SQLx migrations
└── scripts/ Dev tooling
└── dev-setup.sh
```