README
¶
Taskemon
Taskemon is a self-hosted task manager with hidden Pokémon rewards and optional ESC/POS thermal printing. Creating a task generates a reward through PokéAPI; completing the task reveals it and adds it to the user's collection.
This repository contains the Go backend and its command-line setup client. Web and Home Assistant clients can use the HTTP API, but they are not included here.
Project status
Taskemon is pre-1.0 software. Its API, configuration, and database schema may still change between minor releases.
The backend currently includes account management, browser sessions, API keys, task ownership, rewards, statistics, SQLite persistence, and USB thermal-printer support. It does not include an official web frontend, Home Assistant integration, Docker image, OpenAPI specification, or CI workflow.
Features
- First-run setup for the initial administrator and API key
- Intentional public registration for standard user accounts
- Argon2id password hashing and login rate limiting
- Browser sessions with CSRF protection
- Bearer API keys suitable for scripts and Home Assistant
- Administrator account and credential management
- Per-user task creation, bulk creation, listing, updating, completion, deletion, and printing
- Hidden Pokémon rewards revealed only when tasks are completed
- Pokémon collection and task statistics
- SQLite storage with embedded Goose migrations
- Optional direct USB ESC/POS printing with configurable ticket layout
- Explicit CORS origins, security headers, and trusted-proxy handling
Requirements
- Go 1.25.7 or newer
- A C compiler,
pkg-config, and the libusb 1.0 development files when building the current project - Network access to
https://pokeapi.cowhen creating tasks - Optional: a compatible USB ESC/POS printer and permission to access its USB device
The SQLite driver is implemented in Go. A separately installed SQLite library is not required.
Installation
Clone and build the project:
git clone https://github.com/Renan-M-Fernandes/taskemon.git
cd taskemon
go mod download
make build
The binary is written to build/taskemon.
To install it under /usr/local/bin:
sudo make install
The installation prefix can be overridden with the Make variable PREFIX.
Quick start
Copy the example configuration:
cp config.example.json config.json
The example disables physical printing. Review the server address, CORS origins, cookie security, and database path before exposing the service outside a trusted local network.
Build and start the server:
make build
./build/taskemon serve --config config.json
On an empty database, the server logs a one-time initial setup token. Keep the server running, open another terminal, and run:
./build/taskemon setup \
--url http://localhost:8080 \
--username admin \
--display-name "Administrator" \
--timezone America/Sao_Paulo \
--api-key-name "Home Assistant"
The command securely prompts for the setup token and password, then asks for confirmation. The generated API key is displayed once; save it before closing the terminal.
Check the service:
curl http://localhost:8080/api/v1/health
Use the saved API key:
curl \
-H 'Authorization: Bearer tsk_change-me' \
http://localhost:8080/api/v1/tasks/open
Configuration
Configuration file
Taskemon reads JSON configuration:
taskemonstarts the server withconfig.json.taskemon servealso usesconfig.json.taskemon serve --config /path/to/config.jsonuses another file.- If the selected file does not exist, Taskemon runs with built-in defaults.
A partial configuration overrides the corresponding defaults. Unknown fields, invalid JSON, and multiple JSON values are rejected. Relative database paths are resolved from the server's working directory.
No configuration field is required because every field has a default. Initial administrator setup is still required before protected API routes can be used.
Environment variables
The application does not currently read environment variables. Use the configuration file and CLI flags. Variables such as VERSION, PREFIX, DESTDIR, and GO are Makefile inputs, not runtime application settings.
Core defaults
| Setting | Default | Notes |
|---|---|---|
server.host |
0.0.0.0 |
Listens on every interface. |
server.port |
8080 |
Must be between 1 and 65535. |
server.corsOrigins |
["http://localhost:8123"] |
Exact HTTP or HTTPS origins only; wildcards are rejected. |
server.trustedProxyCIDRs |
[] |
Proxies allowed to supply client addresses through X-Forwarded-For. |
auth.secureCookies |
false |
Set to true when browser sessions are served through HTTPS. |
auth.sessionLifetimeDays |
30 |
Allowed range: 1–365 days. |
auth.sessionIdleHours |
168 |
Must be positive and no longer than the total session lifetime. |
auth.maximumConcurrentPasswordWork |
4 |
Allowed range: 1–64. Limits concurrent password hashing and verification. |
database.path |
./database/taskemon.db |
Parent directories are created automatically. |
Only configure trustedProxyCIDRs for proxies you control. Forwarding headers from other clients are ignored.
Printer defaults
Printing is optional. When printer.enabled is false, Taskemon uses the no-op printer and task print requests return successfully without sending data to hardware.
| Setting | Default | Notes |
|---|---|---|
printer.enabled |
false |
Enables physical printer initialization. |
printer.transport |
noop |
Supported values: noop and usb. |
printer.vendorID |
0x0418 |
Hexadecimal USB vendor ID. |
printer.productID |
0x5011 |
Hexadecimal USB product ID. |
printer.endpoint |
1 |
USB output endpoint; allowed range: 1–15. |
printer.devicePath |
/dev/usb/lp0 |
Parsed for compatibility, but the current USB implementation selects devices by vendor ID, product ID, and endpoint. |
printer.cutCommand |
gs_v_0 |
Supported values: gs_v_0, full, partial, and feed. |
printer.qrMode |
pokemon_placeholder |
Use pokemon_placeholder or task_completion. |
printer.baseURL |
empty | Base URL for task_completion QR values. Empty uses taskemon://tasks/{id}/complete. |
printer.layout.paperWidth |
80 |
Retained in configuration; line wrapping is currently controlled by charsPerLine. |
printer.layout.charsPerLine |
48 |
Must be positive. |
printer.layout.feedLinesBeforeCut |
1 |
Allowed range: 0–255. |
Layout sections are header, title, description, tag, qr, shinyHint, and footer. Their full defaults are in config.example.json.
- Text
sizevalues must be between 1 and 8. justifyacceptsleft,center, orright.header.stylemust be between 0 and 5.qr.sizemust be between 1 and 16.qr.correctionacceptsL,M,Q, orH.maxLines: 0means no line limit.- The existing JSON key is spelled
linesAllwaysVisible; use that spelling in configuration.
The two QR modes behave as follows:
| Mode | Printed value |
|---|---|
pokemon_placeholder |
A Pokémon.com URL for the task's generated reward. This can reveal the reward before task completion. |
task_completion |
{baseURL}/tasks/{taskID}/complete, or the taskemon:// form when baseURL is empty. |
CLI usage
taskemon
taskemon serve [options]
taskemon setup [options]
taskemon help
Running taskemon without arguments is equivalent to starting the server with config.json.
serve
taskemon serve [--config path]
| Flag | Default | Description |
|---|---|---|
--config |
config.json |
Path to the JSON configuration file. |
Example:
taskemon serve --config /etc/taskemon/config.json
The server handles SIGINT and SIGTERM and allows up to 10 seconds for graceful HTTP shutdown.
setup
taskemon setup [options]
The server must already be running with an empty user database. The password is always read from an interactive terminal and cannot be supplied through a flag or redirected standard input.
| Flag | Default | Description |
|---|---|---|
--url |
http://localhost:8080 |
Base URL of the running API; paths, queries, and fragments are rejected. |
--token |
prompt | One-time setup token printed by the server. |
--username |
prompt | Initial administrator username. |
--display-name |
username | Administrator display name. |
--timezone |
UTC |
IANA timezone, for example America/Sao_Paulo. |
--api-key-name |
Initial API key |
Name assigned to the generated API key. |
--yes |
false |
Skip the final confirmation. Password input is still interactive. |
Fully specified example:
taskemon setup \
--url https://taskemon.example.com \
--token change-me \
--username admin \
--display-name "Administrator" \
--timezone America/Sao_Paulo \
--api-key-name "Home Assistant" \
--yes
API
All routes are under /api/v1. JSON request bodies must use Content-Type: application/json, cannot exceed 1 MiB, cannot contain unknown fields, and must contain exactly one JSON value. Date-time fields use RFC 3339.
Errors use:
{
"error": "error message"
}
Authentication labels
| Label | Requirement |
|---|---|
| Public | No authentication. |
| Session | A valid taskemon_session cookie. Mutating requests also require X-CSRF-Token. |
| Either | A browser session or Authorization: Bearer <api-key>. API-key requests do not use CSRF. |
| Admin session | An administrator browser session. API keys are not accepted for admin routes. |
Public and setup routes
| Method | Route | Auth | Request | Success response |
|---|---|---|---|---|
GET |
/health |
Public | — | 200 health object |
GET |
/setup |
Public | — | `200 {"required":true |
POST |
/setup |
Setup token | SetupRequest plus X-Taskemon-Setup-Token |
201 user and initial API key |
POST |
/users |
Public | CreateUserRequest |
201 standard user |
POST |
/auth/login |
Public | LoginRequest |
200 user and CSRF token; sets session and CSRF cookies |
POST |
/auth/logout |
Public | Current session cookie when present | 204; revokes that session and clears cookies |
Public registration is intentional. POST /users creates a non-administrator account. Complete initial setup before exposing the API because setup is only available while no users exist.
Current account and credentials
| Method | Route | Auth | Request | Success response |
|---|---|---|---|---|
GET |
/me |
Either | — | 200 current user |
PATCH |
/me |
Session | UserUpdateRequest |
200 updated user |
PUT |
/me/password |
Session | PasswordChangeRequest |
204; revokes all sessions and clears cookies |
POST |
/me/disable |
Session | — | 204; revokes credentials and clears cookies |
DELETE |
/me |
Session | — | 204; deletes the account and owned data |
GET |
/me/api-keys |
Session | — | 200 API-key metadata array |
POST |
/me/api-keys |
Session | CreateAPIKeyRequest |
201 metadata and one-time token |
PATCH |
/me/api-keys/{keyID} |
Session | APIKeyUpdateRequest |
200 updated metadata |
DELETE |
/me/api-keys/{keyID} |
Session | — | 204 |
GET |
/me/sessions |
Session | — | 200 active session array |
DELETE |
/me/sessions |
Session | — | 204; revokes all sessions and clears cookies |
DELETE |
/me/sessions/{sessionID} |
Session | — | 204; clears cookies if the current session was revoked |
GET |
/me/collection |
Either | — | 200 collection array |
GET |
/me/stats |
Either | — | 200 task and collection statistics |
Administrator routes
Every administrator route requires an administrator browser session and CSRF on mutations.
| Method | Route | Request | Success response |
|---|---|---|---|
GET |
/admin/users |
— | 200 enabled user array |
POST |
/admin/users |
CreateUserRequest |
201 standard user |
GET |
/admin/users/disabled |
— | 200 disabled user array |
GET |
/admin/users/lookup/by-username/{username} |
— | 200 user |
GET |
/admin/users/{userID} |
— | 200 user |
POST |
/admin/users/{userID}/enable |
— | 200 enabled user |
POST |
/admin/users/{userID}/disable |
— | 204 |
POST |
/admin/users/{userID}/promote |
— | 204 |
POST |
/admin/users/{userID}/demote |
— | 204 |
DELETE |
/admin/users/{userID} |
— | 204 |
GET |
/admin/users/{userID}/api-keys |
— | 200 API-key metadata array |
DELETE |
/admin/users/{userID}/api-keys/{keyID} |
— | 204 |
Taskemon prevents disabling, deleting, or demoting the last enabled administrator. Administrators manage accounts and credentials but do not receive access to another user's tasks.
Task routes
These routes accept a browser session or API key. The authenticated user ID is taken from the credential, not from the URL or request body.
| Method | Route | Request | Success response |
|---|---|---|---|
GET |
/tasks |
— | 200 all owned tasks |
GET |
/tasks/open |
— | 200 open owned tasks |
GET |
/tasks/completed |
— | 200 completed owned tasks |
GET |
/tasks/{taskID} |
— | 200 task |
POST |
/tasks |
CreateTaskRequest |
201 task |
POST |
/tasks/bulk |
Array of 1–100 CreateTaskRequest values |
201 all succeeded, 207 mixed result, or 400 all failed |
PATCH |
/tasks/{taskID} |
TaskUpdateRequest |
200 updated task |
DELETE |
/tasks/{taskID} |
— | 204; completed tasks cannot be deleted |
POST |
/tasks/{taskID}/complete |
— | 200 completed task with revealed reward |
POST |
/tasks/{taskID}/print |
— | 204 after the configured printer returns |
Request bodies
| Name | JSON fields |
|---|---|
SetupRequest |
username, displayName, password, apiKeyName, timezone |
CreateUserRequest |
username, displayName, password, timezone |
LoginRequest |
username, password |
UserUpdateRequest |
At least one of displayName or timezone |
PasswordChangeRequest |
currentPassword, newPassword |
CreateAPIKeyRequest |
name, optional future expiresAt |
APIKeyUpdateRequest |
name |
CreateTaskRequest |
Required title; optional description, dueAt, and tag |
TaskUpdateRequest |
Any of title, description, dueAt, or tag; send "dueAt": null to clear the due date |
Usernames are normalized to lowercase and must contain 3–64 characters. Passwords must contain 15–128 valid UTF-8 characters. Empty display names default to the username during account creation, empty timezones default to UTC, and timezones must be valid IANA names. API-key names must contain 1–100 characters.
An omitted or empty task tag becomes misc. Task tags are normalized to lowercase. A task response always contains reward.revealed; Pokémon details are omitted until completion.
API-key examples
Create a task:
curl --fail-with-body \
-X POST http://localhost:8080/api/v1/tasks \
-H 'Authorization: Bearer tsk_change-me' \
-H 'Content-Type: application/json' \
-d '{
"title": "Clean the desk",
"description": "Clear the desk before coding",
"dueAt": "2026-08-08T18:00:00-03:00",
"tag": "home"
}'
The initial response hides the reward:
{
"id": 1,
"userID": 1,
"title": "Clean the desk",
"description": "Clear the desk before coding",
"completed": false,
"dueAt": "2026-08-08T21:00:00Z",
"tag": "home",
"createdAt": "2026-08-07T12:00:00Z",
"completedAt": null,
"reward": {
"revealed": false
}
}
Complete it:
curl --fail-with-body \
-X POST http://localhost:8080/api/v1/tasks/1/complete \
-H 'Authorization: Bearer tsk_change-me'
List the collection:
curl --fail-with-body \
-H 'Authorization: Bearer tsk_change-me' \
http://localhost:8080/api/v1/me/collection
Browser-session example
Log in and store the cookies:
curl --fail-with-body \
-c taskemon.cookies \
-X POST http://localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{
"username": "admin",
"password": "change-me-to-a-long-password"
}'
Copy the csrfToken from the response. Use it for unsafe methods:
curl --fail-with-body \
-b taskemon.cookies \
-X PATCH http://localhost:8080/api/v1/me \
-H 'Content-Type: application/json' \
-H 'X-CSRF-Token: change-me' \
-d '{"displayName":"New display name"}'
Logging in while the browser already has a Taskemon session cookie replaces only that session after the new credentials are accepted. Sessions on other devices are not revoked.
Authentication and authorization
- Passwords are stored with Argon2id.
- Browser authentication uses the
taskemon_sessionHttpOnly cookie. - The
taskemon_csrfcookie and login response expose the CSRF token that browser clients must echo inX-CSRF-Tokenfor unsafe methods. - Both cookies use
SameSite=Lax;auth.secureCookiescontrols theirSecureflag. - API keys use the
tsk_prefix and are stored as hashes. The full token is returned only when created. - API keys can access task routes and read current-user data. They cannot manage browser sessions, passwords, API keys, accounts, or administrators.
- Disabled users cannot authenticate. Disabling an account revokes its API keys and sessions.
- Login attempts are limited in memory by source IP and username.
- Expired browser sessions are cleaned at startup and every 24 hours.
Database and migrations
Taskemon uses SQLite in WAL mode with foreign keys enabled, a five-second busy timeout, immediate write transactions, UTC timestamps, and embedded Goose migrations.
On startup, the server:
- Creates the database parent directory when needed.
- Connects to SQLite.
- Applies pending embedded migrations.
- Refuses to start if it detects Taskemon tables without Goose migration history.
The schema contains:
usersapi_keysweb_sessionstaskstask_rewardscollection_entriesuser_statistics
Deleting a user cascades to their credentials, tasks, rewards, collection, and statistics.
Upgrading from v0.3
The pre-authentication v0.3 schema used text user IDs. The current server cannot safely map those IDs to authenticated accounts and intentionally refuses to migrate that database automatically.
Back up the existing database and start this version with an empty database. There is currently no supported in-place v0.3 data migration.
Stop the server before making a simple file-level SQLite backup, and preserve the database together with any active -wal and -shm files if they exist.
Thermal printer
The current physical printer transport uses gousb to select a device by vendor ID and product ID, claim its default interface, open the configured output endpoint, and send ESC/POS commands.
Taskemon attempts USB kernel-driver auto-detach. On Linux, a busy printer may require:
sudo modprobe -r usblp
The service account must also have permission to access the USB device. The repository does not currently include a udev rule or printer diagnostic command.
Development
Run the standard local checks:
make check
This runs gofmt -w ., go mod tidy, go test ./..., go vet ./..., and the release-style build. Because it writes formatting and module changes, review the working tree afterward.
Individual targets:
make test
make vet
make build
make lint
make lint/licenses
make licenses
make lintruns golangci-lint using.golangci.yml.make lint/licensesrequires the REUSE tool.make licensesuses go-licenses 1.6.0 and updatesLICENSE-3rdparty.csv.
SQL queries are defined under internal/database/queries, the schema is under internal/database/migrations, and generated sqlc code is committed under internal/database/db. After changing SQL, regenerate it with a compatible sqlc generate command.
The repository does not currently contain automated CI workflows.
Running tests
go test ./...
go vet ./...
The test suite covers command parsing, setup, configuration, authentication, middleware, account lifecycle, SQLite integration and constraints, API routes, task ownership, rewards, printer rendering, ESC/POS output, and task printing. Hardware-independent printer behavior is tested without requiring a physical USB printer.
Run the race detector when changing shared state, authentication, or HTTP code:
go test -race ./...
Building release binaries
Build the default binary:
make build
Supply another version:
make build VERSION=0.9.0
The Makefile builds ./cmd/taskemon as a position-independent executable with paths trimmed and symbols stripped. Release builds still depend on the target platform's libusb runtime.
Useful Make variables:
| Variable | Default | Purpose |
|---|---|---|
VERSION |
0.9.0 |
Version value passed to the linker. |
OUTDIR |
build |
Build output directory. |
PREFIX |
/usr/local |
Installation prefix. |
DESTDIR |
empty | Packaging root used by make install. |
GO |
go |
Go command. |
Deployment
No Dockerfile, Compose file, systemd unit, or packaged container image is included.
For a production deployment:
- Build the binary on a system with the required Go, C, pkg-config, and libusb dependencies.
- Store
config.jsonand the SQLite database in persistent locations. - Run behind an HTTPS reverse proxy when using browser sessions.
- Set
auth.secureCookiestotrue. - Configure exact frontend origins in
server.corsOrigins. - Configure only the reverse proxy's networks in
server.trustedProxyCIDRs. - Restrict access to the setup token, API keys, database, and server logs.
- Grant USB access only when physical printing is enabled.
The API sets X-Frame-Options: DENY. Use a separate client that calls the API rather than embedding the API directly in an iframe.
Project structure
cmd/taskemon/ CLI dispatcher, server command, and setup client
internal/api/ HTTP routes, handlers, DTOs, CORS, and security headers
internal/auth/ Accounts, passwords, sessions, API keys, and middleware
internal/config/ JSON configuration defaults and validation
internal/database/ SQLite connection, Goose migrations, SQL, and sqlc output
internal/escpos/ ESC/POS command implementation
internal/printer/ Printer transports and ticket rendering
internal/task/ Task, reward, collection, and statistic services
internal/taskprint/ Task-to-printer integration and QR construction
database/ Default runtime database directory
Makefile Build, test, lint, install, and license targets
config.example.json Complete safe example configuration
sqlc.yaml sqlc generation configuration
Security considerations
- Complete initial setup before exposing the server. Public account creation is enabled, and setup stops being available after any user exists.
- The setup token is generated at startup, kept in memory, and printed to server logs only while setup is required. It changes when the server restarts.
- Use HTTPS and secure cookies for browser clients outside local development.
- Store API keys as secrets. They remain valid until revoked or expired.
- Use explicit CORS origins. Wildcard origins are rejected because credentialed browser requests are supported.
- Do not trust proxy CIDRs you do not control.
- The default host exposes the server on all interfaces.
- The default configuration is intended for local development, not direct Internet exposure.
- Pokémon reward creation sends requests to the external PokéAPI service.
Troubleshooting
Build fails for gousb or pkg-config
Install a C compiler, pkg-config, and your platform's libusb 1.0 development package. These are build requirements even when runtime printer support is disabled.
Server reports a legacy or unmanaged database
The server refuses to modify pre-authentication or untracked schemas. Back up the database and use an empty database path. Automatic migration from v0.3 is not supported.
Setup token is rejected
Use the latest token printed by the currently running server. Restarting an unconfigured server generates a new token. Setup returns a conflict after any user exists.
API returns 401 unauthorized
For API-key requests, use exactly:
Authorization: Bearer tsk_change-me
For browser requests, include the session cookie.
Browser mutation returns 403 invalid csrf token
Read the csrfToken from the login response or taskemon_csrf cookie and send it in X-CSRF-Token. API-key requests do not need this header.
Browser request fails CORS preflight
Add the frontend's exact origin, including scheme and port, to server.corsOrigins. Paths and wildcards are invalid.
Task creation returns a service error
Task creation calls PokéAPI to obtain the current species count, Pokémon data, and species data. Confirm outbound HTTPS access to pokeapi.co.
Printer is busy or access is denied
Check the USB IDs and endpoint. On Linux, release usblp if it claimed the interface and grant the service account access to the USB device.
Contributing
Read CONTRIBUTING.md, keep changes focused, update tests and documentation with behavioral changes, and run make check before submitting a pull request.
The active repository is github.com/Renan-M-Fernandes/taskemon.
License
Taskemon is distributed under the EUPL-1.2. Third-party dependency licenses are listed in LICENSE-3rdparty.csv, with license texts under LICENSES/.
Credits
Taskemon started from the thermal-printer task manager created by James Pond. This version expands the original project into a multi-user API with authentication, gamified Pokémon rewards, SQLite persistence, and optional thermal printing.
Pokémon data is retrieved from PokéAPI.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
taskemon
command
|
|
|
internal
|
|
|
escpos
Package escpos is a small vendored ESC/POS implementation based on github.com/hennedo/escpos, patched for generic POS-80 thermal printers.
|
Package escpos is a small vendored ESC/POS implementation based on github.com/hennedo/escpos, patched for generic POS-80 thermal printers. |