This document describes the security model of the Atomic Habit Tracker, its known limitations, and what would be required before any production deployment. It summarizes the full review in issue #1 (OWASP Top 10, 2021).
The app is a client-only Nuxt SPA (ssr: false) with no backend:
- All data — habits, entries, coaching suggestions, settings — lives in the browser's IndexedDB, stored unencrypted, on a single device.
- There is no server, no API, and no network transmission of user data.
- This eliminates entire classes of server-side vulnerability, but shifts the risk to the client and to whoever can access the browser/device.
These are deliberate trade-offs for a local-first demo app, not bugs to be surprised by:
- The auth gate is not a security boundary. Authentication is a single
localStorageflag (app/utils/auth/dummy-auth.ts) checked by client-side route middleware (app/middleware/auth.global.ts). Anyone with the browser, devtools, or any script on the same origin can bypass it. It exists for UX, not protection. See ADR-0007. - Data is unencrypted at rest and readable by any code on the same origin or anyone with device access.
- No multi-user isolation. All data shares one namespace; logout does not clear data, so a shared device exposes the previous user's data.
- Reminders/notifications are best-effort and depend on granted permission and the app being open. See ADR-0008.
If this app (or a fork) is ever deployed for real users, the review flagged these as the priority hardening items — tracked in issue #1:
- Real authentication (an identity provider with server-side session validation) — or remove the auth gate entirely rather than imply protection (SEC-01, SEC-02).
- Consider encrypting data at rest and clearing data on logout for shared devices (SEC-05, SEC-13).
Findings from the review that have since been mitigated within the local-first model:
- Bounded import size and collection counts (SEC-06, SEC-09). Imported JSON is now
bounded before it can freeze the tab or exhaust storage: a 64 MiB pre-read
File.sizecheck rejects an over-large file before it is read, and Zod.max()constraints plus a cheap raw-count preflight cap the habit/entry/suggestion collections and each habit'sscheduleWeekdays/pausesarrays. String-field max-lengths (FIELD_LIMITS) and calendar date bounds already shipped; together these keep a crafted or accidental payload from driving unbounded work or storage. An over-limit import is rejected as a whole — never partially merged or truncated — and current data is left unchanged. Seeapp/types/app-data.ts,app/utils/persistence/storage-schema.ts, andapp/pages/app/settings.vue. - Dev-only tooling gated out of production builds (SEC-10). Nuxt DevTools
(
devtools: { enabled: isDev }) and the PWAdevOptionsare both keyed toNODE_ENV !== 'production', sonuxt build/nuxt generatenever ship them. Seenuxt.config.ts. - HTTP security headers (SEC-11). The generated
.htaccesssetsContent-Security-Policy(self-origin scripts/styles/images/fonts/connect/worker/manifest,object-src 'none',frame-ancestors 'none';'unsafe-inline'is still required for Nuxt/Vite's inline bootstrap and Nuxt UI's injected styles),X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy,Permissions-Policy, and HSTS. The file is rendered frompublic/.htaccess.tplinto.output/public/by thenitro:initclose hook innuxt.config.tsand mirrored to the host bydeploy-production. Apachemod_headersmust be enabled on the host for the headers to take effect. - Session timeout (SEC-03). The dummy-auth session now has an absolute 7-day expiry stamp; expired/malformed sessions read as logged-out and clear their stale keys. See ADR-0011. (This is hygiene, not access control — the gate is still bypassable by design.)
- Service-worker update consent (SEC-14).
registerTypeis now'prompt': new workers download but only activate after the user confirms via a reload banner. See ADR-0008. - Security event logging (SEC-16). A lightweight, in-memory client-side event log
(
app/utils/observability/security-log.ts) records auth, import/export, deletion, validation-failure, and storage events to a bounded ring buffer + console. No network, no persistence. - Storage-quota / write-failure notice (SEC-18). Persistence write failures (especially
QuotaExceededError) and a best-effortnavigator.storage.estimate()pre-check now surface a user-facing warning toast instead of onlyconsole.error(app/composables/use-storage-health.ts).
This is a personal/educational project. To report a security concern, open a GitHub issue
labelled type: security (or contact the maintainer directly for anything sensitive). Please
describe the issue, affected files, and reproduction steps.