# Pins

Keep a link, a note, an image — in seconds — and actually find it again later.

Pins replaces the habit of sending things to yourself on Facebook Messenger. That habit works
because it's effortless to add to, and fails because nothing ever comes back out: no organisation,
no search, everything lost in a scroll. Pins keeps the effortless part and fixes the rest.

Personal project, built for one person. It may become public later; it isn't designed around that.

**Status: kickoff complete, nothing built yet.** The stack below is decided, not implemented.

- Product intent and the things we deliberately won't build → `VALUES.md`
- How we build and ship → `PLAYBOOK.md` (vendored, don't edit here)
- Agent operating file → `CLAUDE.md`

## What it does

- **Capture fast.** Text, links and images. On Android, via the app *and* the system share sheet —
  the same gesture as sharing to Messenger or Mail.
- **Organise with tags.** Flat tags, no folders. Tagging is optional and can happen later or never.
- **Search.** Full-text across everything.
- **Work offline.** Pins are written locally first and synced to the server in the background.
  Capture never waits for the network.

### v1

Android app + web app, backed by the server. Text and links first; images are in v1.0 but not the
first priority.

### Later

- Chrome extension — one-click capture of a page or a note.
- Wear OS companion — voice notes from the watch.
- AI-assisted tag suggestions *(undecided — see Open questions in `VALUES.md`)*.

## Stack

| Piece | Choice |
| --- | --- |
| Web + Android | React + TypeScript, one codebase, wrapped in **Capacitor** |
| Windows | The web app, installed as a PWA (no Electron) |
| Backend | **Laravel** + **MySQL** on Apache |
| Wear OS *(later)* | Native Kotlin + Compose — Capacitor can't target Wear OS |

One React codebase serves web and Android because this is evening work by one person, and a
codebase its author stays fluent in is the one still alive in five years. Ionic's UI kit is not
used — Capacitor is here as the native bridge only. Reasoning for each call is in `CLAUDE.md`.

## Hosts

- **Server:** self-hosted Raspberry Pi (Apache + MySQL, boots from USB SSD), full root access.
  Moves to off-site hosting if Pins ever reaches an app store.
- **Web:** `pins.blommemix.dk` *(provisional)*
- **Android:** sideloaded APK for v1. Later: self-hosted APK server with in-app update checks.
- **Environments:** production only for now. A dev environment follows once prod is live.

## Running it

The frontend is scaffolded and builds. There's no product code yet — it's still Vite's starter page.

```sh
cd app && npm install
npm run dev      # web dev server
npm run build    # type-check + build → app/dist
```

Android:

```sh
cd app && npm run build && npx cap sync android
cd android && JAVA_HOME="D:/Programs/Android Studio/jbr" ./gradlew assembleDebug
# → app/build/outputs/apk/debug/app-debug.apk
```

The backend isn't scaffolded — it's blocked on knowing the Pi's PHP version (see below).

## Bootstrapping a new machine

Per `PLAYBOOK.md` §14 — what a fresh machine needs.

| Tool | Version | Notes |
| --- | --- | --- |
| Node | 25.9 (npm 11.12) | |
| **JDK** | **21** | **Not optional.** Capacitor 8 requires it. Android Studio's bundled JBR works; a JDK 17 on `PATH` fails with the misleading `invalid source release: 21`. |
| Android SDK | via Android Studio | `ANDROID_HOME` must be set |
| PHP + Composer | match the Pi | 8.3.1 / 2.5.8 on the current dev box |
| Git Bash | | `scripts/sync-playbook` is POSIX `sh`, not PowerShell |

Secret files needed locally: none yet. The Laravel `.env` arrives with the backend.

## Environment

Server config will live in Laravel's `.env` on the Pi. A committed `.env.example` lists every
required key with no values — keep the two in sync.

## Secrets inventory

Per `PLAYBOOK.md` §6 — **names and locations only, never values.** None of these are ever committed.

| Secret | Where it lives | Notes |
| --- | --- | --- |
| `.env` (Laravel app key, DB credentials) | The server, outside git | `.env.example` documents the keys |
| SSH deploy key `pins_deploy` | `~/.ssh/pins_deploy` on the dev box, never in git | Connects to the restricted `bmx-pins@admin.blommemix.dk` gate. No passphrase (non-interactive deploys); safety comes from the scoped account + command whitelist, not the key. Public half is in the gate's `authorized_keys`. |
| Android release keystore | Password manager + NAS backup | **Irreplaceable** — lose it and the app can never be updated again. Doesn't exist until the first release build. |
| Wear OS device token | Server-issued, on-device | Watch talks to the server directly, with its own credential |
| AI API key *(if ever)* | Server only | Never in a client — a key in an APK is a leaked key |

## Backups

The pins *are* the product, so this is load-bearing rather than housekeeping.

**Planned, not yet built** (owner returns ~2026-07-30): nightly cron backing up the full server to
the owner's NAS. Offsite redundancy via the parents' NAS, with a server at Gigahost.dk as the
interim copy.

Two conditions, both from `PLAYBOOK.md` §10.1:

- **A restore must actually be tested.** Untested backups fail exactly when they're needed — wrong
  path, MySQL dumped mid-write, cron quietly broken for months. One real restore proves the chain.
- **The NAS is in the same building as the Pi.** Fire, theft or lightning takes both. Offsite isn't
  a nice-to-have here; it's the difference between a scare and losing years of pins.

Until this exists, the server's data is unprotected — see Gotchas in `CLAUDE.md`.

## Provenance

Original idea, arrived at independently (owner, 2026-07-16). Pocket, Pinboard and Raindrop solve
adjacent problems and are worth a look for ideas, but Pins isn't derived from any of them.

## Licence

Not yet decided — worth settling before the repo goes public.
