Files
tessera/docs/scaffold-spec.md
Gjermund Høsøien Wiggen 1136227510 TypeScript/Bun project scaffold
- Stack: Bun, Hono, Drizzle ORM, Zod, Handlebars, Pino
- Models: ticket, queue, transaction, scrip, template, custom_field, user, lifecycle
- Scrip engine: prepare/commit two-phase dispatch, template rendering, mock actions
- Lifecycle validator: state machine transition validation with wildcard support
- Routes: health, tickets (full CRUD + preview + transactions), queues, scrips, custom-fields, lifecycles
- Middleware: Pino logging, error handler
- Database: Drizzle ORM schema + initial migration (10 tables)
- Type-check: passes (tsc --noEmit, zero errors)
2026-06-07 21:21:50 +02:00

9.1 KiB

Scaffold the Tessera Rust project workspace at /home/gjermund/projects/tessera.

What to Create

1. Workspace Cargo.toml

At /home/gjermund/projects/tessera/Cargo.toml:

  • [workspace] with members: crates/tessera-core, crates/tessera-api
  • [workspace.dependencies] with versions for:
    • axum 0.8.x
    • tokio 1.x (full features)
    • serde 1.x (derive feature)
    • serde_json 1.x
    • sqlx 0.8.x (runtime-tokio, tls-rustls, postgres features)
    • uuid 1.x (v4, serde features)
    • chrono 0.4.x (serde feature)
    • tracing 0.1.x, tracing-subscriber 0.3.x
    • tower 0.5.x, tower-http 0.6.x (cors feature)
    • tonic 0.12.x
    • prost 0.13.x
    • anyhow 1.x
    • thiserror 2.x
    • lettre 0.11.x (rustls-tls feature)
    • tera 1.x
    • figment 0.10.x (toml, env features)
    • dotenvy 0.15.x
    • clap 4.x (derive feature)

2. tessera-core crate

At /home/gjermund/projects/tessera/crates/tessera-core/Cargo.toml:

  • name = "tessera-core"
  • Dependencies: serde, serde_json, sqlx, uuid, chrono, anyhow, thiserror, tracing, tera, lettre
  • [lib] crate-type = ["lib"]

At /home/gjermund/projects/tessera/crates/tessera-core/src/lib.rs:

  • Module declarations: pub mod models; pub mod scrip; pub mod lifecycle; pub mod query; pub mod db;
  • Re-export key types

Create these source files (with proper module structure):

crates/tessera-core/src/models/mod.rs

  • Module declarations: mod ticket; mod queue; mod transaction; mod scrip; mod template; mod custom_field; mod user;
  • Re-exports

crates/tessera-core/src/models/ticket.rs

  • Ticket struct with fields: id (Uuid), subject (String), queue_id (Uuid), status (String), owner_id (Option), creator_id (Uuid), created_at (DateTime), updated_at (DateTime), started_at (Option<DateTime>), resolved_at (Option<DateTime>)

crates/tessera-core/src/models/queue.rs

  • Queue struct: id (Uuid), name (String), description (Option), lifecycle_id (Option), created_at (DateTime)

crates/tessera-core/src/models/transaction.rs

  • Transaction struct: id (Uuid), ticket_id (Uuid), transaction_type (String), field (Option), old_value (Option), new_value (Option), data (Option<serde_json::Value>), creator_id (Uuid), created_at (DateTime)

crates/tessera-core/src/models/scrip.rs

  • Scrip struct: id (Uuid), queue_id (Option), name (String), description (Option), condition_type (String), condition_config (serde_json::Value), action_type (String), action_config (serde_json::Value), template_id (Option), stage (String), sort_order (i32), disabled (bool), created_at (DateTime)
  • ScripStage enum: TransactionCreate, TransactionBatch

crates/tessera-core/src/models/template.rs

  • Template struct: id (Uuid), name (String), queue_id (Option), subject_template (String), body_template (String), created_at (DateTime)

crates/tessera-core/src/models/custom_field.rs

  • CustomField struct: id (Uuid), name (String), field_type (String), values (Option<serde_json::Value>), max_values (i32), pattern (Option), created_at (DateTime)
  • QueueCustomField struct: id (Uuid), queue_id (Uuid), custom_field_id (Uuid), sort_order (i32)
  • CustomFieldValue struct: id (Uuid), custom_field_id (Uuid), ticket_id (Uuid), value (String), created_at (DateTime)

crates/tessera-core/src/models/user.rs

  • User struct: id (Uuid), username (String), email (Option), created_at (DateTime)

crates/tessera-core/src/scrip/mod.rs

  • Module declarations: mod engine; mod conditions; mod actions; mod templates;
  • Re-export ScripEngine, PreparedScrip

crates/tessera-core/src/scrip/engine.rs

  • ScripEngine struct with methods:
    • new(pool: PgPool)
    • async fn prepare(&self, ticket_id: Uuid, transactions: &[Transaction]) -> Result<Vec>
      • Load matching scrips from DB (global + queue-specific, filtered by transaction types matching condition_type, sorted by sort_order)
      • For each: evaluate condition → if matches, build PreparedScrip with template substitution
    • async fn commit(&self, prepared: Vec) -> Result<Vec>
      • Execute each action, record results
  • PreparedScrip struct: scrip_id, scrip_name, action_type, action_payload (serde_json::Value), dry_run bool
  • ScripResult struct: scrip_id, success bool, message String

crates/tessera-core/src/scrip/conditions.rs

  • ConditionEvaluator trait with fn evaluate(&self, ticket: &Ticket, transactions: &[Transaction]) -> bool
  • OnCreate condition (true if any transaction is type "Create")
  • OnStatusChange condition (true if any transaction is type "StatusChange")
  • OnResolve condition (true if any transaction changes status to a "resolved" lifecycle state)

crates/tessera-core/src/scrip/actions.rs

  • ActionExecutor trait with async fn execute(&self, payload: &serde_json::Value) -> Result
  • SendEmail action (placeholder — logs the email it would send)
  • Webhook action (placeholder — logs the webhook it would fire)
  • SetCustomField action (placeholder — logs the CF it would set)

crates/tessera-core/src/scrip/templates.rs

  • TemplateRenderer struct that uses Tera
  • async fn render(&self, template: &Template, context: &serde_json::Value) -> Result<(String, String)> — returns (subject, body)

crates/tessera-core/src/lifecycle/mod.rs

  • Lifecycle struct: id (Uuid), name (String), definition (serde_json::Value)
  • LifecycleValidator struct with methods:
    • fn validate_transition(&self, lifecycle_def: &serde_json::Value, from: &str, to: &str) -> Result<()>
    • fn is_valid_status(&self, lifecycle_def: &serde_json::Value, status: &str) -> bool

crates/tessera-core/src/query/mod.rs

  • Placeholder module with comment: "TicketSQL query builder — post-MVP"

crates/tessera-core/src/db/mod.rs

  • pub mod migrations;
  • Pub async fn run_migrations(pool: &PgPool) -> Result<()>

crates/tessera-core/src/db/migrations.rs

  • Function signatures for migration runner (placeholder — actual migrations in sql files)

3. tessera-api crate

At /home/gjermund/projects/tessera/crates/tessera-api/Cargo.toml:

  • name = "tessera-api"
  • Dependencies: tessera-core (path), axum, tokio, serde, serde_json, uuid, anyhow, tracing, tracing-subscriber, tower-http, clap, figment, dotenvy
  • bin name = "tessera-api"

At /home/gjermund/projects/tessera/crates/tessera-api/src/main.rs:

  • Clap CLI with subcommands: serve, migrate
  • serve: start axum HTTP server
  • migrate: run database migrations
  • tracing_subscriber init with JSON formatting
  • Load config from TESSERA_CONFIG env or tessera.toml

Create source files:

crates/tessera-api/src/routes/mod.rs

  • Module declarations: mod health; mod tickets; mod queues; mod scrips; mod custom_fields; mod lifecycles;

crates/tessera-api/src/routes/health.rs

  • GET /health → 200 with {"status": "ok", "version": "0.1.0"}

crates/tessera-api/src/routes/tickets.rs

  • Router function returning axum Router with placeholder handlers:
    • GET /api/tickets
    • POST /api/tickets
    • GET /api/tickets/:id
    • PATCH /api/tickets/:id
    • POST /api/tickets/:id/preview
    • GET /api/tickets/:id/transactions
    • POST /api/tickets/:id/comment
  • Each handler returns 501 NotImplemented with a descriptive message

crates/tessera-api/src/routes/queues.rs

  • GET /api/queues → 501
  • POST /api/queues → 501

crates/tessera-api/src/routes/scrips.rs

  • GET /api/scrips → 501
  • POST /api/scrips → 501
  • GET /api/scrips/:id → 501
  • PATCH /api/scrips/:id → 501

crates/tessera-api/src/routes/custom_fields.rs

  • GET /api/custom-fields → 501
  • POST /api/custom-fields → 501

crates/tessera-api/src/routes/lifecycles.rs

  • GET /api/lifecycles → 501
  • POST /api/lifecycles → 501

crates/tessera-api/src/config.rs

  • AppConfig struct: database_url (String), server_host (String), server_port (u16)
  • impl from figment

4. SQL Migrations

At /home/gjermund/projects/tessera/migrations/0001_initial_schema.sql:

  • All CREATE TABLE statements from the architecture document (tickets, queues, lifecycles, transactions, scrips, templates, custom_fields, queue_custom_fields, custom_field_values, users)
  • CREATE INDEX statements on: tickets(queue_id), tickets(status), transactions(ticket_id), transactions(created_at), scrips(queue_id), custom_field_values(ticket_id), custom_field_values(custom_field_id)

5. Configuration

At /home/gjermund/projects/tessera/tessera.example.toml:

[database]
url = "postgres://tessera:tessera@localhost:5432/tessera"

[server]
host = "127.0.0.1"
port = 8080

6. .gitignore

Standard Rust .gitignore: target/, .env, *.log

Rules

  • Use cargo init where appropriate, or create files manually
  • All code must compile: run cargo check --workspace after creating everything
  • Fix any compilation errors
  • Use cargo fmt and cargo clippy on the workspace
  • Do NOT create a separate Cargo.lock — it will be generated
  • The workspace Cargo.toml must NOT have a [package] section

Verification

After creating all files, run:

  1. cargo check --workspace — must succeed
  2. cargo fmt --check — must pass
  3. cargo clippy --workspace — should pass or have only minor warnings

If any verification fails, fix the issues and re-run until clean.