| .opencode | ||
| cmd/myday | ||
| deploy | ||
| docs | ||
| internal | ||
| .gitignore | ||
| AGENT.md | ||
| config.example.yaml | ||
| go.mod | ||
| go.sum | ||
| PROJECT_PLAN.md | ||
| README.md | ||
MyDay
A self-hosted employee time, absence & reporting app by Sechpoint. Delivered as a PWA for mobile and desktop. Single-tenant — one company per instance.
Status: 🚧 M0 + M0.1 + M0.2 implemented — the auth & routing foundation, the web/PWA layer and
the admin & account UI (the SPEC-002…010 slices) are built: routing · identity/sessions · the door ·
/setup · enroll/login (+ the passkey nudge) · roles/chain/recovery · the module registry · the web
shell · the role-gated nav, the account page (add a passkey · opt-out), the admin Users page
(create + invite) and the passkey banner.
Next milestone: M1 — org structure. The design of record is
PROJECT_PLAN.md.
What it will do
| Area | |
|---|---|
| Org structure | users, the manager hierarchy, departments, positions |
| Time | presence and timesheets (a per-user mode) |
| Absence | leave requests, approvals, balances |
| Reporting | rollups and aggregations |
Built as one Go binary: a small core (MyDay.auth — identity, sessions, the door) plus
drop-in modules.
Running it
# 1. configure
cp config.example.yaml config.yaml # then edit; config.yaml is gitignored
# 2. build
go build -o myday ./cmd/myday
# 3. run
./myday
⚠️ First run — enable setup, then complete the wizard
The first user becomes the top admin (the root of the chain). Setup is gated by the setup
config toggle (D-045): enable it, start MyDay, complete the wizard — the app then writes
setup: false and the instance becomes productive.
# config.yaml
setup: true # enable the /setup wizard
setup_code: "" # OPTIONAL second factor — set it if the domain is reachable by others
- Start MyDay. The startup banner states the mode and prints the setup URL.
- Open the wizard at
/setup(e.g.https://myday.sechpoint.app/setup).setup_codeempty → the wizard is served directly;setup_codeset → the code form comes first; enter the code printed in the startup banner.
- Complete the wizard — domain → top admin (TOTP) → mail/locale. It creates the admin and
writes
setup: falsetoconfig.yaml. - Restart so the app adopts the persisted secrets:
systemctl restart myday.
While setup: true and setup is unfinished:
GET /points at the wizard, andGET /setupserves it (or the code form);- every other route answers 404 — there is no app yet.
While setup: false and no admin exists, the instance is locked: GET / and GET /setup
show a page telling you to set setup: true and restart, and every other route 404. This state is
not publicly claimable — a fresh instance never exposes its bootstrap by accident.
Residual risk: while
setup: true, whoever reaches/setupfirst can claim the admin. Setsetup_code(or complete setup immediately) if the domain is open to others. The code is single-use and the entry form is rate-limited.
Read the code again (when one is set):
journalctl -u myday # inside the container
pct exec 11020 -- journalctl -u myday # from the Proxmox host
Only after the top admin exists should you put MyDay behind a reverse proxy and expose it.
Note: the setup wizard enrolls TOTP; the admin adds a passkey at first sign-in on the real (https) origin, via the passkey nudge.
Deployment — behind Zoraxy (TLS at the proxy)
Full walkthrough:
docs/deploy-zoraxy.md— topology · config · the Zoraxy ProxyHost · the firewall rule · thesetup: truebootstrap · systemd · the misconfig warnings. This section is the summary.
The reference deployment terminates TLS at Zoraxy at
10.1.0.10; the app itself speaks plain HTTP. Config (config.example.yaml →
config.yaml, D-043):
- App —
server.listen: "0.0.0.0:8080"; the app binds any address (there is no setup-mode bind refusal). - Proxy — Zoraxy terminates TLS and forwards to
http://<app-host>:8080, settingX-Forwarded-ForandX-Forwarded-Proto. trusted_proxies—["10.1.0.10"](Zoraxy). The app honoursX-Forwarded-Protofrom a trusted proxy to decide a request ishttps, and then sets the session cookieSecure+__Host-and sends HSTS. Over plain http it keeps the bare, non-Securecookie. A wrong value fails silently — noSecurecookie, no HSTS.base_url—https://myday.sechpoint.app: the public origin, used in invite/recovery links.- Firewall —
:8080MUST be reachable only from10.1.0.10. Anything else that can reach the port bypasses the proxy and its TLS; the app would then serve plain HTTP to that client.
# on a fresh instance: enable setup, start MyDay, then open the domain at /setup
# config.yaml: setup: true
# (optional) setup_code: "…" # a second factor; the printed code comes first
sudo systemctl restart myday
# read the mode / setup code in the log:
journalctl -u myday # inside the container
pct exec 11020 -- journalctl -u myday # from the Proxmox host
What /setup writes — and the restart
The wizard persists config.yaml (setup: false, the base URL, mail/locale, and the secrets)
next to the loaded file — never over the committed config.example.yaml. If the file is not
writable it prints the values for you to save by hand, and setup still completes.
The secrets are generated at startup when a value is still CHANGE_ME…, so the wizard's
ceremony is never signed with a known key. The persisted file then holds those same values —
so a restart adopts them and the admin's TOTP secret (sealed with the generated key) keeps
working. Boot a productive instance from the written config.yaml, not from the example:
config.example.yaml's placeholders fail closed in productive mode by design.
Documentation
PROJECT_PLAN.md |
the plan & decision log of record |
AGENT.md |
instructions for AI agents working here |
docs/specs/ |
implementation specs |
docs/deploy-zoraxy.md |
the Zoraxy deployment guide (D-043) |
config.example.yaml |
configuration template |
Security
- Secrets live in
config.yaml(gitignored) — never commit them. - Auth is Passkey (primary) + TOTP (fallback).
- See
PROJECT_PLAN.md§10 for the deferred-security register and its beta gate.
License
TBD.