Ticketing system built for schools. Staff and students submit tickets; maintainers manage them and configure the deployment at runtime.
Ticketa is a single binary that serves both the REST API and the compiled React frontend. The frontend (Ticketa-client) is cloned and built inside the Docker image — no pre-built assets are committed here. The final image is based on scratch and contains only the binary.
Docker build stages
1. frontend-builder — node:22-alpine — clones Ticketa-client, runs npm build
2. builder — golang:1.24-alpine — embeds compiled frontend via //go:embed, builds binary
3. runtime — scratch — contains only the statically linked binary
The database is PostgreSQL. Migrations run automatically at startup; no external migration tool is required.
| Role | Description |
|---|---|
student |
Can create and manage their own tickets |
staff |
Can create and manage their own tickets |
maintainer |
Full access — manages all tickets, users, statuses, and runtime config |
Interactive documentation is served at /docs once the server is running.
Authentication is cookie-based (session_token, HTTP-only). Authenticated and admin routes return 401 without a valid session cookie and 403 when the role requirement is not met. All error responses share the same shape:
{ "code": 404, "status": "Not Found", "msg": "ticket not found" }No authentication required.
Create a local account.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | Valid e-mail address |
password |
string | yes | Min. 8 chars, must include uppercase, digit, and special character |
user_type |
string | yes | student, staff, or maintainer |
first_name |
string | no | |
last_name |
string | no |
Responses
| Status | Description |
|---|---|
201 |
{ "id": 42 } |
400 |
Missing field, weak password, or unknown user type |
500 |
Internal error (e.g. duplicate e-mail) |
Authenticate and receive a session cookie.
Request body
| Field | Type | Required |
|---|---|---|
email |
string | yes |
password |
string | yes |
Responses
| Status | Description |
|---|---|
200 |
Sets session_token HTTP-only cookie (7-day TTL) |
400 |
Invalid request body |
401 |
Wrong credentials or inactive account |
Requires a valid session_token cookie.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | Short summary |
body |
string | yes | Full description |
status_id |
integer | no | ID of the initial status |
Responses: 201 ticket object · 400 bad body · 401 no session · 422 missing title or body · 500
Returns all tickets ordered newest first. Empty result returns [].
Responses: 200 array of ticket objects · 401 · 500
Responses: 200 ticket object · 400 bad ID · 401 · 404 not found · 500
Only the author can update. All fields are optional in the body.
Request body: title, body, status_id (all optional)
Responses: 200 updated ticket · 400 · 401 · 403 not the author · 404 · 500
Only the author can delete.
Responses: 204 deleted · 400 · 401 · 403 not the author · 404 · 500
Requires a valid session_token cookie and user_type = maintainer.
Responses: 200 config object · 401 · 403
Changes are written atomically to /config/ticketa.yaml on the host and take effect immediately without a restart. Providing ticket_statuses requires at least 3 entries.
Request body (all optional)
{
"logging": { "level": "debug", "dir": "/var/log/ticketa" },
"ticket_statuses": [
{ "title": "Otevřeno", "color": "#3498db" },
{ "title": "Probíhá", "color": "#f39c12" },
{ "title": "Vyřešeno", "color": "#2ecc71" }
]
}Responses: 200 updated config · 400 bad body or disk write failure · 401 · 403
Returns statuses ordered by position. Empty result returns [].
Responses: 200 array · 401 · 403 · 500
Also appends the new status to the YAML config.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | |
color |
string | no | HEX format, e.g. #9b59b6. Defaults to #808080 |
position |
integer | no | Must be unique |
Responses: 201 status object · 400 · 401 · 403 · 422 missing title · 500
Syncs change to YAML config.
Request body: title, color (both optional)
Responses: 200 updated status · 400 · 401 · 403 · 404 · 500
Tickets referencing this status will have status_id set to null. YAML config is synced.
Responses: 204 · 400 · 401 · 403 · 500
Responses: 200 array of user objects · 401 · 403 · 500
Responses: 200 user object · 400 · 401 · 403 · 404 · 500
Request body (all optional)
| Field | Type | Notes |
|---|---|---|
is_active |
boolean | Inactive users cannot log in |
user_type |
string | student, staff, or maintainer |
Responses: 200 updated user · 400 · 401 · 403 · 404 · 500
Ticket
{
"ID": 1,
"Title": "Nemohu se přihlásit",
"Body": "Po zadání hesla se nic nestane.",
"CreatedAt": "2026-06-07T14:22:55Z",
"AuthorID": 3,
"StatusID": { "Int32": 0, "Valid": false }
}TicketStatus
{ "ID": 1, "Title": "Probíhá", "Color": "#f39c12", "Position": 1 }User
{
"ID": 3,
"Email": "jan.novak@skola.cz",
"FirstName": { "String": "Jan", "Valid": true },
"LastName": { "String": "Novák", "Valid": true },
"UserType": "student",
"Provider": "local",
"IsActive": true,
"CreatedAt": "2026-06-07T12:00:00Z",
"LastLoginAt": { "Time": "0001-01-01T00:00:00Z", "Valid": false }
}Config
{
"Logging": { "Level": "info", "Dir": "/var/log/ticketa" },
"TicketStatuses": [
{ "Title": "Otevřeno", "Color": "#3498db" },
{ "Title": "Probíhá", "Color": "#f39c12" },
{ "Title": "Vyřešeno", "Color": "#2ecc71" }
]
}Configuration is split into two layers:
Copy .env.example and fill in your values:
cp .env.example .env| Variable | Default | Description |
|---|---|---|
PG_HOST |
database |
Postgres hostname |
PG_PORT |
5432 |
Postgres port |
PG_USER |
— | Postgres user (required) |
PG_PASSWORD |
— | Postgres password (required) |
PG_DATABASE |
ticketa |
Postgres database name |
SERVER_PORT |
8080 |
HTTP port the server listens on |
LOG_LEVEL |
info |
Overrides the log level from YAML (info or debug) |
Controls logging and ticket statuses. The file is volume-mounted from the host so changes written via the admin API persist through container restarts.
cp config/ticketa.yaml.example config/ticketa.yamllogging:
level: info # debug | info
dir: /var/log/ticketa
ticket_statuses:
- title: "Otevřeno"
color: "#3498db"
- title: "Probíhá"
color: "#f39c12"
- title: "Vyřešeno"
color: "#2ecc71"Rules for ticket_statuses:
- Minimum three statuses required
- First = open state, last = resolved state, any middle = in-progress
- Order in the array determines the
positionstored in the database - Changes via
PATCH /api/admin/configare written back to this file automatically
git clone https://github.com/StepanKomis/Ticketa.git
cd Ticketa
cp .env.example .env
cp config/ticketa.yaml.example config/ticketa.yaml
# edit both files with your values
make docker-build
make deployThe app is available at http://localhost:8080 once the containers are healthy. Interactive API docs are at http://localhost:8080/docs.
Requirements: Go 1.24+, Docker, sqlc (for query regeneration).
# Start only the database
docker compose up -d database
# Build and run locally (frontend not embedded)
make build
./build/ticketa
# Full local build including frontend
make build-full
./build/ticketa| Target | Description |
|---|---|
build |
Compile Go binary to ./build/ticketa |
build-frontend |
Clone Ticketa-client, build it, copy dist/ into the embed directory |
build-full |
build-frontend + build — full local build including the frontend |
run-local |
Start database via Docker, then run the local binary |
docker-build |
Build Docker image (with layer cache) |
docker-build-nc |
Build Docker image without cache |
deploy |
Start all services via docker compose up -d |
test |
Run the Go test suite |
sqlc |
Regenerate database query code via sqlc |
swagger-ui |
Download pinned Swagger UI dist assets into src/www/docs/ |
swag |
Regenerate swagger.yaml from swag annotations in handler source files |
clean |
Remove ./build and the frontend embed directory |
Migrations are embedded in the binary and run automatically on startup. The schema includes:
users— accounts with role (student,staff,maintainer) and active flagsessions— HTTP-only cookie sessions with expiryticket_statuses— ordered list of statuses (position-aware, synced with YAML config)tickets— support tickets with title, body, author reference, and optional status
config/
ticketa.yaml.example runtime config template
src/
cmd/
main.go entrypoint — loads config, starts server
server/
logs/ structured file logger
startup/ server bootstrap (DB connect, migrate, listen)
config/ YAML config types, loader, atomic writer, thread-safe Store
database/postgres/
migrations/ embedded SQL migrations (UP_000N.sql)
queries/ sqlc-generated type-safe query functions
internal/
ctxkeys/ shared context key types (avoids import cycles)
security/ session store, token generation, cookie helpers
www/
docs/ embedded Swagger UI assets + swagger.yaml (generováno přes make swag)
midleware/ AuthMiddleware, MaintainerMiddleware
router/
handlers/ UserHandler, TicketHandler, AdminHandler, StaticHandler, DocsHandler
router.go route registration
embed.go //go:embed for static frontend assets
docs_embed.go //go:embed for Swagger UI assets