Reviewed-on: #16
MiauInv
MiauInv is a lightweight inventory, stock, and project allocation management system written in Go. It provides a server-rendered dashboard with HTMX-style page composition, vanilla JavaScript for API interactions, SQLite for persistence, JWT-based sessions, refresh-token rotation, account settings, and optional TOTP-based two-factor authentication and WebAuthn passkey authentication.
The project is designed for self-hosted/private deployments. It is not a full enterprise asset-management platform, but it already covers the main workflows needed for tracking items, locations, stock distribution, and project allocations.
Contents
- Features
- Current Status
- Technical Stack
- Architecture
- Documentation
- Configuration
- Routes and API Endpoints
- Setup
- Docker Deployment
- Reverse Proxy Deployment
- Security Notes
- Screenshots
Features
Inventory and allocation
- Item management with name, category, description, and total quantity.
- Location management for physical or logical storage places.
- Stock mapping between items and locations.
- Project management for allocating items to projects.
- Association tracking between projects and item quantities.
- Dashboard statistics for items, locations, and projects.
Account and authentication
- User registration, if enabled in the configuration.
- Password hashing with bcrypt.
- Signed JWT access tokens.
- Database-backed refresh tokens with rotation.
- HTTP-only secure cookies for access and refresh tokens.
- Account settings page at
/profile/settings. - Passkey registration, login, removal, and full passkey disable from account settings.
- Username change with password confirmation.
- Password change with old-password verification and session refresh.
Two-factor authentication
- Optional TOTP 2FA using authenticator apps.
- QR-code based setup from the account settings page.
- Manual setup key fallback if QR scanning is not available.
- Two-step login flow for accounts with 2FA enabled.
- Recovery codes generated when 2FA is enabled.
- Recovery codes are stored only as hashes.
- Recovery codes can be downloaded as a text file after generation.
- Recovery codes can be regenerated from account settings.
- Recovery-code count warnings in account settings.
- Recovery codes are one-time use.
- The setup secret is only stored after the first valid authenticator code.
- Existing refresh sessions are revoked when 2FA is enabled or disabled.
Passkeys
- WebAuthn-based passkey registration from account settings.
- Passkey login from the normal sign-in page.
- Discoverable passkey login without entering a username first.
- User-verification required for registration and login.
- Server-side challenge storage using opaque one-time challenge tokens.
- Passkey removal with current-password confirmation.
- Full passkey disable with current-password confirmation.
- Existing refresh sessions are revoked when passkeys are added, removed, or disabled.
Current Status
MiauInv is an active private project. The current version supports core inventory workflows and account-level security settings. Some areas are intentionally still basic:
- There is no dedicated admin panel yet.
- Basic in-memory rate limiting protects login, 2FA, refresh, registration, and sensitive account endpoints.
- Automated testing is currently limited and will be expanded in future releases.
- The application currently uses native TLS. If deployed behind a reverse proxy, the proxy must connect to the backend over HTTPS or the backend TLS behavior must be adjusted intentionally.
Technical Stack
| Area | Technology |
|---|---|
| Backend | Go 1.26 |
| Routing | Go standard library net/http |
| Database | SQLite via github.com/glebarez/go-sqlite |
| Authentication | JWT via github.com/golang-jwt/jwt/v5 |
| Password hashing | bcrypt via golang.org/x/crypto/bcrypt |
| 2FA | TOTP via github.com/pquerna/otp/totp |
| Passkeys | WebAuthn via github.com/go-webauthn/webauthn |
| Frontend | Server-rendered HTML, HTMX-style structure, vanilla JavaScript |
| Styling | Custom CSS with dark theme variables |
| Deployment | Docker / Docker Compose |
Architecture
The codebase is split into small packages with mostly direct responsibilities:
| Path | Responsibility |
|---|---|
main.go |
Application entrypoint. Initializes configuration, database, and server startup. |
server/ |
HTTP route registration, TLS listener, and server-level configuration. |
config/ |
Runtime configuration file creation and loading. |
auth/ |
JWT generation, JWT validation, middleware, role middleware, and password helpers. |
handlers/ |
JSON API handlers for authentication, account settings, inventory, locations, projects, stock, and associations. |
storage/ |
SQLite schema setup, migrations, and database access helpers. |
models/ |
Shared data structures and constants. |
frontend/ |
HTML template rendering and static asset serving. |
frontend/assets/js/ |
Frontend API client, login flow, token refresh logic, account settings logic, and dashboard actions. |
frontend/htmx/ |
HTML views and dashboard content templates. |
Documentation
More detailed documentation is available in:
Configuration
MiauInv reads ./appdata/config.yaml. If the file does not exist, the application creates a default configuration on startup.
port: "8080"
database_path: ./appdata/database.db
certificate_path: ./appdata/cert.pem
private_key_path: ./appdata/key.pem
allow_registration: true
Configuration fields
| Field | Description |
|---|---|
port |
HTTPS listen port. |
database_path |
SQLite database path. |
certificate_path |
TLS certificate path. |
private_key_path |
TLS private key path. |
allow_registration |
Enables or disables public registration. If false, /register renders a blocked-registration page and /api/register is not registered. |
Environment variables
| Variable | Required | Description |
|---|---|---|
JWT_SECRET |
Yes | Symmetric signing secret for JWTs. Must be at least 32 characters. |
Generate a local development secret with:
openssl rand -base64 48
Routes and API Endpoints
Frontend routes
| Route | Method | Auth required | Description |
|---|---|---|---|
/ |
GET |
No | Landing page. |
/login |
GET |
No | Login page. Supports password login and the second 2FA step. |
/register |
GET |
No | Registration page or blocked-registration page, depending on configuration. |
/dashboard |
GET |
Yes | Dashboard overview. |
/inventory |
GET |
Yes | Stock and inventory overview. |
/items |
GET |
Yes | Item management view. |
/locations |
GET |
Yes | Location management view. |
/projects |
GET |
Yes | Project management view. |
/profile/settings |
GET |
Yes | Account settings, password changes, passkey management, 2FA setup, 2FA disable, and recovery-code management. |
/profile/ |
GET |
No | Placeholder page for unfinished profile subpages. |
/assets/* |
GET |
No | Static CSS/JS assets. Minified CSS/JS variants are generated on request. |
Authentication and account API
| Endpoint | Method | Auth required | Description |
|---|---|---|---|
/api/register |
POST |
No | Creates a user if registration is enabled. |
/api/login |
POST |
No | Validates username/password. Returns a full session if 2FA is disabled; otherwise returns a short-lived 2FA challenge token. |
/api/login/2fa |
POST |
No | Completes login using the 2FA challenge token plus either a TOTP code or a recovery code. |
/api/refresh |
POST |
No | Rotates a refresh token. Accepts the token from JSON or the refresh_token cookie. |
/api/logout |
POST |
Yes | Revokes refresh tokens for the current user and clears auth cookies. |
/api/profile |
GET |
Yes | Returns current user metadata, 2FA state, and unused recovery-code count. |
/api/userinfo |
GET |
Yes | Same user information handler as /api/profile. |
/api/account/username |
POST |
Yes | Changes the current username after password confirmation. |
/api/account/password |
POST |
Yes | Changes the current password, revokes old refresh tokens, and issues a new session. |
/api/2fa/setup |
POST |
Yes | Creates a pending TOTP secret and returns secret, setup_token, otpauth_url, and a base64 PNG QR code. |
/api/2fa/enable |
POST |
Yes | Enables 2FA after validating the temporary setup token and a TOTP code. Replaces recovery codes and revokes old sessions. |
/api/2fa/disable |
POST |
Yes | Disables 2FA after password and TOTP confirmation. Revokes sessions and clears auth cookies. |
/api/2fa/recovery-codes/regenerate |
POST |
Yes | Invalidates existing recovery codes and returns a new set after password and TOTP confirmation. |
/api/passkeys |
GET |
Yes | Lists passkeys registered for the current user. |
/api/passkeys |
DELETE |
Yes | Removes one passkey after current-password confirmation. |
/api/passkeys/register/options |
POST |
Yes | Creates a server-side passkey registration challenge after current-password confirmation. |
/api/passkeys/register/finish |
POST |
Yes | Verifies and stores a new passkey credential. Revokes old sessions and issues a new current session. |
/api/passkeys/login/options |
POST |
No | Creates a discoverable passkey login challenge. |
/api/passkeys/login/finish |
POST |
No | Verifies passkey login and issues a full session. |
/api/passkeys/disable |
POST |
Yes | Deletes all passkeys after current-password confirmation. Revokes old sessions and issues a new current session. |
Inventory API
| Endpoint | Method | Auth required | Query parameters | Description |
|---|---|---|---|---|
/api/item |
GET |
Yes | id optional |
Returns all items with aggregate quantities, or one item by ID. |
/api/item |
POST |
Yes | None | Creates an item. |
/api/item |
PUT |
Yes | id required |
Updates item fields. |
/api/item |
DELETE |
Yes | id required |
Deletes an item if no dependent rows block it. |
/api/location |
GET |
Yes | id, content optional |
Returns all locations, one location, or location contents with content=true. |
/api/location |
POST |
Yes | None | Creates a location. |
/api/location |
PUT |
Yes | id required |
Renames a location. |
/api/location |
DELETE |
Yes | id required |
Deletes a location if no dependent rows block it. |
/api/project |
GET |
Yes | id, details optional |
Returns all projects, one project, or project allocations with details=true. |
/api/project |
POST |
Yes | None | Creates a project. |
/api/project |
PUT |
Yes | id required |
Updates a project. |
/api/project |
DELETE |
Yes | id required |
Deletes a project if no dependent rows block it. |
/api/stock |
GET |
Yes | id optional |
Returns stock entries. |
/api/stock |
POST |
Yes | None | Creates a stock entry. |
/api/stock |
PUT |
Yes | id required |
Updates a stock entry. |
/api/stock |
DELETE |
Yes | id required |
Deletes a stock entry. |
/api/association |
GET |
Yes | id, project_id optional |
Returns project-item associations. |
/api/association |
POST |
Yes | None | Allocates item quantity to a project. |
/api/association |
PUT |
Yes | id required |
Updates a project allocation. |
/api/association |
DELETE |
Yes | id required |
Removes a project allocation. |
Setup
Prerequisites
- Go 1.26.
- SQLite-compatible environment.
- OpenSSL or another way to generate TLS certificates for local development.
- A
JWT_SECRETwith at least 32 characters.
Generate development TLS files
The server uses http.ListenAndServeTLS, so TLS files must exist before startup.
mkdir -p appdata
openssl req -x509 -newkey rsa:4096 \
-keyout appdata/key.pem \
-out appdata/cert.pem \
-days 365 \
-nodes \
-subj "/CN=localhost"
Native local run
export JWT_SECRET="replace-this-with-a-random-secret-of-at-least-32-chars"
go mod tidy
go build -o miauinv .
./miauinv
Then open:
https://localhost:8080
Docker Deployment
The repository contains a multi-stage Dockerfile. The final image is based on scratch and contains the compiled binary plus frontend assets.
Example docker-compose.yaml:
services:
miauinv:
image: git.miaurizius.de/miaurizius/miauinv:latest
container_name: MiauInv
restart: unless-stopped
ports:
- "8080:8080"
environment:
- JWT_SECRET=replace-this-with-a-random-secret-of-at-least-32-chars
volumes:
- ./appdata:/appdata
Start the container:
docker compose up -d
View logs:
docker compose logs -f
Reverse Proxy Deployment
MiauInv currently listens with native TLS. If it is deployed behind Caddy or another reverse proxy, the proxy must connect to the backend using HTTPS unless the server code is changed to listen without TLS.
Example Caddy configuration with a self-signed backend certificate:
inv.example.com {
encode zstd gzip
reverse_proxy https://miauinv:8080 {
transport http {
tls_insecure_skip_verify
}
}
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
}
For Docker deployments, place Caddy and MiauInv on the same Docker network and reverse proxy to the service name.
Security Notes
- JWTs are signed, not encrypted. Normal access and purpose tokens must not contain secrets. The temporary 2FA setup token is a narrow exception because it carries the not-yet-enabled TOTP secret back to the authenticated browser until confirmation.
JWT_SECRETmust be random and private.- Access tokens expire after 15 minutes.
- Refresh tokens expire after 7 days and are rotated on refresh.
- Refresh tokens and recovery codes are stored in the database as hashes.
- Passkey credentials store public-key credential data, not private keys. Private keys remain in the authenticator or platform passkey provider.
- TOTP secrets are currently stored in the database because the server must validate codes. Protect the database file accordingly.
- Recovery codes are only shown when generated. Users should download or copy them immediately. The UI warns when few unused codes remain.
- 2FA disable and recovery-code regeneration require both the current password and a valid TOTP code.
- Basic in-memory rate limiting is enabled for login, passkey ceremonies, 2FA, refresh, registration, and sensitive account endpoints. Use persistent or distributed rate limiting for multi-instance deployments.
- Automated testing is currently limited. Authentication, 2FA, recovery codes, rate limiting, account settings, and inventory handlers should be covered before production use.
Screenshots
Dashboard
Inventory
Locations
Projects