diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..a32cfc9 --- /dev/null +++ b/docs/architecture.md @@ -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 +```