The live instance had no way to add a passkey or a user. Two core pages now
make the signed-in experience usable (SPEC-010):
* the ACCOUNT page (GET /{GUID}/account) shows the user's factors (passkey
count · TOTP enrolled), an "Add a passkey" button that LINKS to the
existing D-041 ceremony screen — the WebAuthn flow is never duplicated —
and a "Don't ask again" control writing users.passkey_opt_out;
* the admin USERS page (GET /{GUID}/users) lists every user (name · email ·
role · manager · identity status) and creates one: only roles below the
creator (D-019), the manager defaulting to the creator, then users.Create
-> identities.Mint -> the /{GUID} invite is EMAILED and shown for
copy/paste.
Both ride the door as core dispatch modules, so the door still gates them
(session + role + CSRF). The entrypoint requires admin for the users
functions; account stays at the team floor.
Additive support: auth.Identities.TOTPEnrolled, users.Users.List,
server.Deps.Credentials (the banner's "has a passkey?" read) and the
CoreDispatchNames guard now covers account/users.
|
||
|---|---|---|
| .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 implemented — the auth & routing foundation and the web/PWA layer
(the SPEC-002…009 slices) are built: routing · identity/sessions · the door · /setup ·
enroll/login (+ the passkey nudge) · roles/chain/recovery · the module registry · the web shell.
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.