Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ permissions:
jobs:
quality:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17.6-alpine
env:
POSTGRES_USER: betterticket
POSTGRES_PASSWORD: betterticket_ci
POSTGRES_DB: betterticket
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U betterticket -d betterticket"
--health-interval 5s
--health-timeout 3s
--health-retries 12
env:
DATABASE_URL: postgresql://betterticket:betterticket_ci@localhost:5432/betterticket
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
Expand All @@ -22,13 +38,17 @@ jobs:
- run: pnpm format:check
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm db:migrate
- run: pnpm test
- run: pnpm coverage
- run: pnpm build
- uses: actions/upload-artifact@v7
if: always()
with:
name: coverage
path: coverage/
path: |
coverage/
apps/*/coverage/
if-no-files-found: ignore

infrastructure-smoke:
Expand Down
12 changes: 7 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

## Status and source of truth

BetterTicket currently has infrastructure only. `infrastructure_plan.md` is the approved source of truth; it records Fastify, a pnpm workspace, Drizzle migrations, a 50% future coverage floor, and deferred provider/release choices. Do not change those decisions without revising the plan through `planning-and-task-breakdown` or `spec-driven-development` first.
BetterTicket has infrastructure plus an anonymous ticket-creation baseline. `infrastructure_plan.md` remains the approved infrastructure source of truth; `docs/specs/`, `docs/plans/`, and `docs/decisions/` record approved product scope and architecture. Do not change those decisions without revising the relevant plan/spec through `planning-and-task-breakdown` or `spec-driven-development` first.

## Repository map

| Area | Location | Status |
| -------------- | ----------------------------------------------------------------- | ------------------------------------ |
| Frontend | `apps/web` | Not created yet; planned React/Vite. |
| API | `apps/api` | Not created yet; planned Fastify. |
| Frontend | `apps/web` | React/Vite ticket creation. |
| API | `apps/api` | Fastify/Drizzle ticket creation. |
| Infrastructure | `compose.yml`, `docker/`, `.env.example` | Present. |
| Scripts | `scripts/smoke.sh` | Present; infrastructure-only. |
| Tests | Application test directories | Not created yet. |
| Tests | Colocated `*.test.ts(x)` files | Unit, component, and DB integration. |
| CI | `.github/workflows/pr-checks.yml`, `.github/workflows/codeql.yml` | Present. |
| Docs | `README.md`, `infrastructure_plan.md` | Present. |
| Docs | `README.md`, `infrastructure_plan.md`, `docs/` | Present. |
| Agent skills | `.agents/skills/` | Present. |

## Required reading and boundaries
Expand All @@ -38,8 +38,10 @@ docker compose --profile tools run --rm toolchain pnpm install --frozen-lockfile
docker compose --profile tools run --rm toolchain pnpm format:check
docker compose --profile tools run --rm toolchain pnpm lint
docker compose --profile tools run --rm toolchain pnpm typecheck
docker compose --profile tools run --rm toolchain pnpm db:migrate
docker compose --profile tools run --rm toolchain pnpm test
docker compose --profile tools run --rm toolchain pnpm coverage
docker compose --profile tools run --rm toolchain pnpm build
bash scripts/smoke.sh
```

Expand Down
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# BetterTicket

BetterTicket is a future public, multi-user web application. This repository currently provides its development infrastructure only; no production frontend, API, authentication, database schema, or product workflows have been implemented.
BetterTicket is a public ticketing web application. The first product slice supports anonymous ticket creation through a React frontend and Fastify API, with tickets stored in PostgreSQL. Authentication and technician workflows are not implemented yet.

## Repository map

| Location | Purpose |
| --------------------------- | -------------------------------------------------------------------- |
| `apps/web` | Planned React/Vite frontend; not created yet. |
| `apps/api` | Planned Fastify API; not created yet. |
| `apps/web` | React/Vite ticket-creation frontend and component tests. |
| `apps/api` | Fastify API, Drizzle schema/migrations, and API/database tests. |
| `docker/` and `compose.yml` | Pinned Docker toolchain and local PostgreSQL/S3-compatible services. |
| `scripts/smoke.sh` | Disposable infrastructure smoke test. |
| `.github/workflows/` | Pull-request checks and scheduled CodeQL analysis. |
| `.agents/skills/` | Repository-specific Codex skills. |
| `docs/` | Product specs, implementation plans, and architecture decisions. |
| `infrastructure_plan.md` | Approved infrastructure decisions and deferred product decisions. |

## Getting started
Expand All @@ -38,32 +39,42 @@ BetterTicket is a future public, multi-user web application. This repository cur
docker compose up --detach postgres object-storage
```

5. Run the available quality checks in the toolchain container, then the host Docker smoke test:
5. Apply the versioned PostgreSQL migrations, then start the frontend and API in the pinned toolchain. Vite serves the application at `http://localhost:5173` and proxies `/api` to Fastify on port 3000:

```sh
docker compose --profile tools run --rm toolchain pnpm db:migrate
docker compose --profile tools run --rm --service-ports toolchain pnpm dev
```

6. Run the quality checks in the toolchain container, then the host Docker smoke test. PostgreSQL must be running because the API suite includes a real persistence test:

```sh
docker compose --profile tools run --rm toolchain pnpm format:check
docker compose --profile tools run --rm toolchain pnpm lint
docker compose --profile tools run --rm toolchain pnpm typecheck
docker compose --profile tools run --rm toolchain pnpm test
docker compose --profile tools run --rm toolchain pnpm coverage
docker compose --profile tools run --rm toolchain pnpm build
bash scripts/smoke.sh
```

With no application tests yet, Vitest validates the configured harness and exits successfully with no test files. Coverage thresholds take effect once application source is added.

6. Stop the long-lived local services when finished:
7. Stop the long-lived local services when finished:

```sh
docker compose down
```

To discard their local data as well, use `docker compose down --volumes`.

## Ticket creation baseline

Anonymous visitors can submit a title, issue description, setup details, and optional additional information. `POST /api/tickets` validates the request, rejects unknown fields, and stores the ticket with a generated UUID, `OPEN` status, and timestamps. See [the feature spec](docs/specs/ticket-creation.md) and [ADR-001](docs/decisions/001-anonymous-ticket-creation.md) for scope and rationale.

## Tooling and CI

The root package is a pnpm workspace reserved for future `apps/web` and `apps/api` packages. React/Vite and Fastify are recorded in the workspace catalog but no application packages or entrypoints exist yet. Drizzle ORM/Kit is available for the future PostgreSQL migration workflow; no business schema or migrations exist.
The pnpm workspace contains React/Vite and Fastify packages. Drizzle schema files are the database source of truth, and generated SQL migrations are committed under `apps/api/drizzle`.

Pull requests run locked installation, formatting, linting, type checking, the empty test harness, coverage, Compose validation, the infrastructure smoke test, and Gitleaks. CodeQL runs weekly and on manual dispatch. Dependabot tracks npm, Docker, and GitHub Actions updates. Playwright, API integration, application builds, Trivy scanning, deployment, and release workflows begin only after application code and provider choices exist.
Pull requests run locked installation, formatting, linting, type checking, migrations against PostgreSQL, unit/component/integration tests, coverage, application builds, Compose validation, the infrastructure smoke test, and Gitleaks. CodeQL runs weekly and on manual dispatch. Dependabot tracks npm, Docker, and GitHub Actions updates. Playwright, Trivy scanning, deployment, and release workflows remain deferred.

Docker image tags are deliberately pinned. Dependabot proposes updates; review and test them before merging.

Expand All @@ -76,4 +87,6 @@ Before enabling a release workflow, select the static host, API container host,
- If Docker cannot connect, start Docker Desktop and rerun `docker compose config --quiet`.
- If ports 5432 or 8333 are occupied, stop the conflicting local service or change the loopback mapping in `compose.yml`.
- If Docker reports a bind-mount permission issue, grant Docker Desktop access to this repository and rerun the command.
- If tests report that the `tickets` relation does not exist, run `docker compose --profile tools run --rm toolchain pnpm db:migrate`.
- If ports 3000 or 5173 are occupied, stop the conflicting application before running the development command.
- If a locked install fails after changing dependencies, regenerate `pnpm-lock.yaml` from the toolchain with `docker compose --profile tools run --rm toolchain pnpm install`, review the diff, then rerun with `--frozen-lockfile`.
14 changes: 14 additions & 0 deletions apps/api/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from "drizzle-kit";

const databaseUrl = process.env.DATABASE_URL;

if (!databaseUrl) {
throw new Error("DATABASE_URL is required to run database commands");
}

export default defineConfig({
dialect: "postgresql",
out: "./drizzle",
schema: "./src/database/schema.ts",
dbCredentials: { url: databaseUrl }
});
11 changes: 11 additions & 0 deletions apps/api/drizzle/0000_yielding_blizzard.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TYPE "public"."ticket_status" AS ENUM('OPEN');--> statement-breakpoint
CREATE TABLE "tickets" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" varchar(160) NOT NULL,
"description" text NOT NULL,
"setup" text NOT NULL,
"additional_information" text,
"status" "ticket_status" DEFAULT 'OPEN' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
91 changes: 91 additions & 0 deletions apps/api/drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"id": "a78acd5c-2d65-420e-aada-4126037ca948",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.tickets": {
"name": "tickets",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "varchar(160)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true
},
"setup": {
"name": "setup",
"type": "text",
"primaryKey": false,
"notNull": true
},
"additional_information": {
"name": "additional_information",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "ticket_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'OPEN'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.ticket_status": {
"name": "ticket_status",
"schema": "public",
"values": ["OPEN"]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
13 changes: 13 additions & 0 deletions apps/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1790627527624,
"tag": "0000_yielding_blizzard",
"breakpoints": true
}
]
}
29 changes: 29 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@betterticket/api",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"coverage": "vitest run --coverage",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"dev": "tsx watch src/server.ts",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"drizzle-orm": "^0.45.2",
"fastify": "catalog:",
"pg": "^8.0.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
"@types/pg": "^8.0.0",
"@vitest/coverage-v8": "^4.0.0",
"drizzle-kit": "^0.31.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0",
"vitest": "^4.0.0"
}
}
53 changes: 53 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Fastify, { type FastifyServerOptions } from "fastify";

import { ticketRoutes } from "./tickets/ticket-route.js";
import type { TicketRepository } from "./tickets/ticket-repository.js";

interface BuildAppOptions {
ticketRepository: TicketRepository;
logger?: FastifyServerOptions["logger"];
}

export function buildApp({ ticketRepository, logger = true }: BuildAppOptions) {
const app = Fastify({
logger,
ajv: {
customOptions: {
removeAdditional: false
}
}
});

app.addHook("onSend", async (_request, reply) => {
void reply
.header("x-content-type-options", "nosniff")
.header("x-frame-options", "DENY")
.header("referrer-policy", "no-referrer");
});

app.setErrorHandler((error, request, reply) => {
if (typeof error === "object" && error !== null && "validation" in error && error.validation) {
return reply.status(400).send({
error: {
code: "VALIDATION_ERROR",
message: "Ticket details are invalid"
}
});
}

request.log.error(
{ errorType: error instanceof Error ? error.name : "UnknownError" },
"ticket request failed"
);
return reply.status(500).send({
error: {
code: "INTERNAL_ERROR",
message: "The ticket could not be created"
}
});
});

void app.register(ticketRoutes, { ticketRepository });

return app;
}
11 changes: 11 additions & 0 deletions apps/api/src/database/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";

export function createDatabase(databaseUrl: string) {
const pool = new Pool({ connectionString: databaseUrl });

return {
database: drizzle(pool),
close: () => pool.end()
};
}
Loading
Loading