Engineering guide for Our Space — a private two-user couples app at github.com/t7sen/our-space (deployed t7senlovesbesho.me, Android me.t7senlovesbesho). LOAD for ANY task in this repo: features, fixes, review, deployment, push routing, biometric gate, dom/sub permissions, Capacitor, Redis, server actions, presence, SSE. Triggers: OurSpace, t7senlovesbesho, Tasks/Rules/Ledger/Notes/Mood/SafeWord, FloatingNavbar, BiometricGate, FCMProvider, PushToast, Honor device, no-GMS, dom/sub, T7SEN, Besho, Sir, kitten. Stack: Next.js 16, React 19, Capacitor 8, Upstash Redis, Firebase Admin, shadcn/ui, Tailwind v4. Hosted-webapp Capacitor shell — server.url loads t7senlovesbesho.me, NOT bundled (no offline, no PWA). Enforces patterns invisible to training: globalThis casts, deferred setState, void vibrate, server-side role checks, FCM-only push, Cairo TZ keys. Skipping produces code that breaks on Android, leaks state, hallucinates removed deps (Serwist, VAPID, web-push), or violates the dom/sub model.
Resources
20Install
npx skillscat add t7sen/our-space Install via the SkillsCat registry.
Our Space — Pre-Flight Skill
This skill complements AGENTS.md (the working summary and architectural overview). When this skill is loaded, run the pre-flight checklist below before any code or proposal. Heavy details live in AGENTS.md and references/*.md — load on demand.
The two highest-value sections are kept inline below: the anti-hallucination inventory (Section 2) and the abridged refusal catalog (Section 3). The full refusal catalog is in references/refusal-catalog.md.
0. Agent Pre-Flight (Run Every Request)
Before writing code or proposing changes, complete this checklist:
- Banned scope check → Does the request mention
gallery,bucket list, orvoice notes/audio recording on/permissions? If yes → refuse, propose/notes//timeline//tasks//rules//ledgerfor the first two, or text + markdown body for the third. - Architecture conflict check → Does the request imply offline support, PWA features, service workers, web push, or removing
server.url? If yes → refuse with rationale fromAGENTS.mdSection 3.7. Do not implement. - Anti-hallucination check → Read Section 2 below before writing imports or env-var references.
- Role-context identification → Does this involve a state mutation? If yes → identify which author (
T7SEN/Besho) is allowed and ensure server-side role check (AGENTS.mdSection 3.1;references/auth-and-security.md). For/permissionsspecifically,getAutoRulesis Sir-only and must return[]for Besho — don't relax. - Reference routing → Use the table in
AGENTS.mdSection 13 to decide whichreferences/*.mdfile to load. Don't skim the body when a reference has the answer. For any/permissionswork, loadreferences/permissions.md. - Date math check → Need a date key, day boundary, or windowing math? Import from
@/lib/cairo-time. Never reinventIntl.DateTimeFormathelpers at a callsite. Never hardcode+02:00— DST drift half the year. - FCM nullability check → Does this touch push delivery? If yes → treat
push:fcm:{author}as nullable perAGENTS.mdSection 3.3, never as guaranteed-present.
When unsure, ask the user one targeted question rather than guessing. Guessing on this codebase produces runtime failures.
1. Critical Patterns to Apply Automatically
Apply without prompting. Full examples in references/coding-patterns.md.
- Browser globals via
globalThis as unknown as { ... }cast — neverwindow/document/navigatordirectly. setStateinside mount-time effects or Capacitor callbacks → wrap insetTimeout(() => setState(...), 0).vibrate()always prefixed withvoid.Date.now()in render →useState(() => Date.now()).'use server'files export only async — constants live insrc/lib/*-constants.ts.cookies()andheaders()are async (Next.js 16) —awaitthem.useSearchParams()requires a<Suspense>boundary at the page level (Next 16 prerender bailout).- Optimistic UI mutations on existing records → snapshot, mutate, server, rollback-on-error, refresh-on-success. Skip for create-paths.
- Redis writes that depend on each other →
redis.pipeline(). - Date-derived keys → Cairo time via
MY_TZ, never UTC. - Mobile-first page padding:
p-4 md:p-12on outer wrappers,gap-4 md:gap-6on grids,pb-28 md:pb-32for floating-navbar clearance. <TabsContent>that holds form-bearing children mustforceMount— Radix unmounts inactive tabs andFormDataignores DOM-absent inputs.- Cards needing 1Hz/60s ticks own their own
setInterval— never tick the dashboard parent. - Custom interactive surfaces (raw
<button>,<Link>) getactive:scale-[0.95]. The shadcn<Button>already has its own press feedback. - Never animate
filter: blur()and never usemode="popLayout"in hot paths. Android WebView doesn't compositefilter— every frame is a full repaint. Stick toopacity+transform. Lone exception:src/app/login/page.tsx(once-per-session). - Glass cards =
backdrop-blur-md+shadow-xl shadow-black/30. Not-xl/-2xl. Heavier glass tanks framerate on Samsung/Honor mid-range. Usewill-change-transformonly on elements that transform every navigation or every second (page-transition wrapper, navbar pill, CounterCard spans). Overuse explodes GPU layer count. - Icon-only buttons need ≥24dp effective hit area. Use
p-1.5minimum for inline icons;p-2for primary actions like panel close. Neveropacity-0 group-hover:opacity-100for actions a mobile user needs to reach — there is no hover. - Form submit success effects call
void hideKeyboard()from@/lib/keyboardso the soft keyboard dismisses with the form. - Mobile-friendly form inputs:
inputMode,enterKeyHint,autoComplete,autoCorrect,autoCapitalize,spellCheck— set them deliberately.<input type="search">for search;autoComplete="current-password"for the login passcode. <body>carriessuppressHydrationWarningto absorb browser-extension-injected attributes; don't remove.- Per-page purge + per-item delete are Sir-only on both client (
currentAuthor === "T7SEN"gate) and server (role check in the action). Use<PurgeButton>from@/components/admin/purge-buttonfor purge UI; mirror the two-step / heavy-haptic pattern for new per-item destructive controls. - Soft-delete is the boundary, not
del. Everydelete*/purgeAll*action callsmoveToTrash/moveManyToTrashfrom@/lib/trashBEFORE the deletion pipeline. 7-day TTL, restorable from/admin/trash. Auxiliary state (reactions, audits, occurrences, streak/count keys, pin-set membership) is intentionally not preserved — only the primary record + index ZSET entry come back. - Activity feed is logger-driven.
logger.interaction/warn/error/fatalautomatically write to theactivity:logZSET (capped at 500). Don't callrecordActivitydirectly; let the logger do it. Sir reads at/admin/activity. - Force-logout = bump
session:epoch:{author}.decrypt()checks JWTiatagainst the epoch (5s in-process cache). All admin destructive actions go through the inspector / push-test / sessions / export / trash / activity surfaces under/admin, gated bysrc/app/admin/layout.tsxredirect +requireSir()insrc/app/actions/admin.ts. summonKitten()is the only Sir → Besho push that mirrors safeword's bypass:bypassPresence: true+channelId: "safeword"+priority: "max"+sound: "default". Possessive/dominant copy is fixed in-action. Surfaced as<SummonButton>on the/adminlanding.<DeviceTracker />(root-layout-mounted) is the sole writer ofdevice:*.pingDeviceruns on mount + 60s heartbeat. Author claim is sticky. Sir reads at/admin/devices. Don't callpingDevicefrom feature code; don't conflate withusePresence.- Restraint mode =
mode:restraint:Besho. Every Besho-writable server action MUST callassertWriteAllowed(session.author)from@/lib/restraintand return its error if non-null. Sir is never restrained. Safeword is intentionally exempt. Toggled from<RestraintToggle>on/adminlanding. auth:failuresis the failed-login ZSET (capped 100). Written exclusively fromlogin()inactions/auth.ts; read viagetAuthFailures()(Sir-only). Don't reuse for unrelated security events.- Project is on Vercel Hobby; there is intentionally no
vercel.json. Hobby rejects builds with cron schedules more frequent than once-per-day. Every cron trigger goes through cron-job.org (configured in the operator's account, minute cadence) hitting/api/cron/*withAuthorization: Bearer ${CRON_SECRET}. New cron-style features should add a new/api/cron/*route + an entry in cron-job.org — never viavercel.json.
2. Things That Do Not Exist (Anti-Hallucination Inventory)
These were removed or never existed. Do not import them, reference them, or write code that uses them. If you find yourself typing one of these — stop.
| Removed / nonexistent | Replacement |
|---|---|
@serwist/next, @serwist/sw, serwist, workbox-* |
None — PWA removed |
web-push package, VAPID_* env vars |
None — Web Push removed |
navigator.serviceWorker, sw.register(), public/sw.js, public/manifest.json |
None |
src/lib/offline-notes.ts, storePendingNote, getPendingNotes, removePendingNote, PendingNote |
None — IndexedDB queue removed |
/api/notes/sync endpoint |
None — only /api/notes/stream (SSE) exists in notes/api/ |
push:subscription:{author} Redis key |
push:fcm:{author} only |
prisma, @prisma/client, SQL migrations |
Upstash Redis is the sole datastore; /src/generated/prisma is a stale gitignore entry |
| Light-mode Tailwind variants | Dark theme is forced via forcedTheme="dark" |
tailwind.config.ts / tailwind.config.js |
Tailwind v4 is CSS-first; tokens live in src/app/globals.css |
pages/ directory, getServerSideProps, getStaticProps |
App Router only |
VoiceNote type, voiceNote field, MediaRecorder + RECORD_AUDIO permission for /permissions |
None — voice notes prototyped on permissions and explicitly removed |
TZ primitives in @/lib/rituals (dateKeyInTz, todayKeyCairo, tzWallClockToUtcMs, previousDateKey, nextDateKey, weekdayOfDateKey) |
@/lib/cairo-time — the migration relocated all TZ math; importing from rituals will fail |
Inline todayInCairo(), secondsUntilMidnight(), or per-callsite Intl.DateTimeFormat date-key helpers |
@/lib/cairo-time exports the canonical versions — never reinvent |
If a search result, training memory, or autocomplete suggests one of these — it is wrong for this codebase. This table is also mirrored in references/anti-hallucination.md for tools that load reference files but not this one.
3. Refusal Catalog (Abridged)
Refuse these immediately with a one-line rationale. Do not implement, do not ask for clarification, do not "try a workaround." Full table (14 rows) in references/refusal-catalog.md.
| Request pattern | Why refuse |
|---|---|
| Add a gallery / photo feature, or bucket list | Banned feature surface |
| Re-add PWA / Serwist / service worker | Removed intentionally; conflicts with server.url |
Re-add Web Push / VAPID / web-push package |
Removed with PWA; conflicts with server.url |
Re-suggest voice notes on /permissions |
Prototyped and explicitly removed |
Use == / != instead of === / !== |
Coercion masks bugs in this strict-mode codebase |
| Skip role check because "the UI hides the button" | Server actions are public endpoints; client is adversarial |
Expose getAutoRules to Besho |
Sir-only authoring artifacts; gaming risk |
dangerouslySetInnerHTML user content |
XSS vector — use MarkdownRenderer |
| Notification dedup / per-author cooldown | Banner pile-up is by design — every event surfaces |
| Rate-limit safeword or permission submissions | Already layered-protected; refuse without observed glitch |
Universal MutationResult typing of all server actions |
Preventive refactor with no observed drift; refuse pre-evidence |
4. Agent Operating Procedure
When this skill triggers, follow this order:
- Run Section 0 pre-flight. Refuse if banned or architecturally incompatible.
- State a plan before code for any non-trivial change. Name the file paths you'll touch and the function/symbol you'll edit.
- Load references on demand per
AGENTS.mdSection 13. Don't rely on memory of patterns when a reference is one tool call away. - Apply Section 1 patterns to every code change automatically. Re-check before submitting.
- Cite file paths when proposing edits. Format:
src/app/notes/page.tsx::handleFormSubmit. - Push back on bad ideas, including from the user. Refuse with rationale; offer alternatives. Do not sugar-coat. Examples: user asks for
==→ refuse, explain coercion. User asks to add Web Push → refuse, point toAGENTS.mdSection 3.7. User asks to skip a server-side role check → refuse, explain client adversariality. - Surface uncertainty. If a request is ambiguous, ask one targeted question. Do not invent context.
- No bugs. Re-read every block of generated code before presenting. "Probably works" is a failure mode.
- Tone: formal, direct, technical. The user is solo-developing this. They want answers, not warmth.
When you finish a non-trivial change, suggest the relevant smoke-test step from references/deployment.md.
5. Where to Find Everything Else
- Architectural pillars (3.1–3.7):
AGENTS.mdSection 3 - Code style, React, TypeScript, naming, UI:
AGENTS.mdSection 5 +references/code-style.md - Auth, security, error handling, accessibility:
AGENTS.mdSection 6 +references/auth-and-security.md - File map:
AGENTS.mdSection 11 - Decision heuristics:
AGENTS.mdSection 12 - Reference index:
AGENTS.mdSection 13