Add architecture document and RT analysis reference
This commit is contained in:
310
docs/architecture.md
Normal file
310
docs/architecture.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user