# APP-DEVELOPMENT.md How Palace apps are built. These principles apply to every app. ## Direction-Driven Development Palace Ring is built through a two-layer system: **direction** and **code**. Junwon works in direction. Coding agents work in code. ``` /direction/ ← Junwon reads and approves specs here /code/ ← Coding agents implement here. Junwon does not review code. ``` **Direction is the source of truth.** A spec in `/direction/` is the product contract. Code that contradicts direction is wrong, regardless of whether it runs. Code that runs but doesn't match direction is not done. **Junwon's role:** Write and approve specs in `/direction/`. Review outcomes at the direction level — does the built thing match the spec? Not: does the code look right? **Coding agents' role:** Read the relevant spec from `/direction/`. Implement in `/code/`. Validate implementation against the spec before reporting done. Report gaps or ambiguities in the spec back up for direction-level resolution — not code-level workarounds. **The workflow:** 1. Spec written or updated in `/direction/` 2. Junwon reads and approves the spec 3. Coding agent receives spec, implements in `/code/` 4. Agent self-validates: does the running app match every point in the spec? 5. Agent reports outcome at direction level: what matched, what didn't, what was ambiguous 6. Junwon approves or updates the spec — never the code directly **What direction specs contain:** Behavior, not implementation. What the user sees and does. What data flows where. What errors look like. Not: which framework, which file, which function. Those are code decisions. **What stays out of direction:** Technology choices, file structure, component names, API shapes. These are code decisions. Direction describes the product; code describes the implementation. ## Data Granularity: Efficiency vs Flexibility When designing how app data is stored, balance token efficiency against flexibility. Both matter. **Efficiency:** Split data into the smallest meaningful units. A trip spanning multiple days should store each day separately, and each entry within a day separately. When Ace needs to add a note to one lunch entry, it should read and write only that entry file (~500 bytes, ~125 tokens) — not the whole trip, not the whole day. Monolithic files that grow without bound are a token tax on every small edit. **Flexibility:** Do not assume structure beyond what the data actually has. A PATCH endpoint that updates known fields (`{ drinks: ["coca-cola"] }`) looks efficient on paper — no file read, just post a delta. But real user input is freeform narrative: "the drive through Big Sur was windy and single-lane and I got stuck behind slow cars twice for 10+ minutes each time." There is no field for that. Designing a PATCH schema for every possible thing a human might say is insane. For freeform content, Ace always needs to read the entry, understand it, and rewrite it naturally. **The right balance:** Granular files (one per entry, one per day) + plain read-modify-write. Small files mean reads are cheap. No PATCH endpoint needed. Works for structured facts and freeform narrative equally. **Rule:** Divide data by natural semantic boundaries (trip → day → entry). Never assume the edit will be structured enough to patch without reading. The goal is minimizing what must be read, not eliminating the read entirely. ## Butler-Driven Design Palace apps are built with the assumption that AI butlers will be helping humans use them. In a palace, human users have AI butlers — the butler helps the user across all the apps in the palace. Junwon's AI butler is Ace. Every app must include a `BUTLER.md` at its root directory (`apps//BUTLER.md`). This file is the first thing any butler reads when it enters the app. It explains: - How the app's data is structured - What files the butler can read and write - Schemas for each data type - Recommended sub-agent assignments for parallel work - Rules to prevent file collisions The file is called `BUTLER.md`, not `AGENTS.md` or `README.md`. It speaks directly to the butler. ## Separation of Code and Data **Hard rule: data never goes inside `apps/`. Ever.** | What | Where | |------|-------| | App code, components, API routes | `apps//` | | User data (JSON files, media, records) | `palaces/manglasabang/palaceappsdata//` | App code lives in `apps//`. User data lives in `palaces/manglasabang/palaceappsdata//`. The palace (`palaces/manglasabang/`) is Junwon's personal data environment — everything he generates, logs, or stores goes there, not inside the app codebase. Code should not embed data; data must be loadable independently so editing it never requires a rebuild or restart. When writing API endpoints, the `DATA` constant must point to `palaces/manglasabang/palaceappsdata//`, not anywhere inside `apps/`. ## Adding a New App Every new app requires all three registration points — not just the code: 1. **`apps.ts`** — Add entry to `APPS` array (`id`, `label`, `href`, `grad`, `icon`) 2. **`home.astro`** — Add the app tile to the correct section on the home screen (Life, Office, Media, Pedia, Lab, Fund, Computer, Energy). The home screen is hardcoded, not auto-generated from `apps.ts`. 3. **Page + API** — Create `palacering/src/pages//index.astro` and API endpoints under `palacering/src/pages/api//` 4. **Data dir** — Create `palaces/manglasabang/palaceappsdata//` 5. **BUTLER.md** — Create `apps//BUTLER.md` 6. **Linear issue** — Track the work If any of these are missing, the app is not done. ## Astro PWA Stack All Palace apps are Astro PWAs. No React Native, no Electron, no native wrappers. The ring (`palacering/`) is the host — individual apps are pages within it. When designing app features, proactively suggest PWA-accessible hardware capabilities that would make the app better. Don't wait for Junwon to ask — surface these during design: - **Offline use** — Service workers, Cache API, IndexedDB for offline-first data - **Camera** — `getUserMedia` for photo/video capture, barcode scanning - **Microphone** — Voice recording, speech-to-text - **Geolocation** — GPS for maps, location-based features - **Push notifications** — Web Push API for alerts - **Device motion/orientation** — Accelerometer, gyroscope - **Vibration** — Haptic feedback on actions - **Share** — Web Share API for native share sheets - **Contacts** — Contact Picker API (Chrome) - **File system** — File System Access API for reading/writing local files - **Bluetooth** — Web Bluetooth for peripherals - **NFC** — Web NFC for tap interactions (Android) - **Clipboard** — Read/write clipboard - **Screen wake lock** — Keep screen on during cooking, meditation, etc. When proposing a new app or feature, include a "PWA capabilities" section listing which of these apply and how they'd enhance the experience. ## Architecture: Build Like Consumer Apps Palace apps are built the way Meta, Instagram, and TikTok build theirs — as a **monolithic Astro SSR app**. All apps are pages within `palacering/`. The nav and butler live at the root layout level and are always present. There is one process, one build, one layout. **Why this matters:** Independent servers per app create an unsolvable shared-UI problem. If nav and butler live in the shell but apps run in separate processes, there is no clean way to show them in every app — iframes break URL routing, inject scripts are third-party widget patterns, and duplicating nav in each app creates permanent maintenance debt. Consumer apps don't have this problem because they don't split into independent servers. Neither should Palace. **The rule:** Every app is a set of pages and API routes inside `palacering/src/pages//`. App code that grows complex lives in `apps//src/` and gets imported by palacering pages — but the server is always palacering. **What lives where:** - `palacering/src/pages//` — page routes (index.astro, subpages) - `palacering/src/pages/api//` — API endpoints - `apps//src/components/` — complex UI components imported by palacering pages - `palaces/manglasabang/palaceappsdata//` — user data **palacering's role:** palacering is the entire app. Nav, butler, home, and every feature page. It owns everything a user touches. **The restart concern is not a valid reason to split.** Astro builds fast. Restarting palacering via launchctl takes seconds. Premature architectural fragmentation to avoid a 10-second restart is not a tradeoff worth making. ## Standard Input — Palace SDK Every app input must use the standard input component from `@palace/sdk`. No app builds its own text field, form bar, or input widget. The SDK input is the same one used in Palace Code, Palace Butler, and Palace Chat — it is the universal way users talk to Palace. The standard input includes: - **Text input** — keyboard entry - **Voice input** — microphone button, speech-to-text, enabled by default - **Processing by Claude Code** — all input is routed through the `claude -p` subprocess bridge, not raw form submission. The butler interprets what the user said and acts on it. This means apps do not have bespoke form controls for adding data. The user speaks or types naturally ("I woke up at 10 and had coffee"), and the butler parses intent, extracts structured data, and writes to the app's data files via the API. The input bar is a conversation with Ace, not a form. When building a new app page, do not create `
` elements or custom input bars. Import and mount the SDK input component. If the SDK input doesn't exist yet, build it — it lives in `palaceplatform/sdk/`. ## Content Prepared by Butlers Data files are prepared by butlers for the user's personal viewing only. Palace apps are self-hosted and private — not published to the public. Copyrighted content (photos, images, articles) can be included in app data when it serves the user. Don't filter by license or restrict to "free" sources. Pick the best content available. This applies to photos in palacemap, articles in palacemagazine, and any other app where the butler curates content for the user. ## URL-Driven State (Deep Linking) Any app page that displays a specific piece of data (a date, a record, a trip) must reflect that in the URL. The URL is the source of truth for what is being viewed — not client-side JS state that disappears on reload. **The pattern:** 1. **Catch-all route** — Use `[...date].astro` (or `[...slug].astro`) instead of a flat `index.astro`. This lets any sub-path render the same page with different initial data. 2. **Server parses the URL** — In the Astro frontmatter, extract the param and convert it to the data key (e.g. `"2026/03/21"` → `"2026-03-21"`). Fall back to today/default if no param. 3. **Pass initial state to client** — Use `define:vars={{ initialDate }}` on the `