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

14 KiB

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)

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