# 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 ```