Build MVP: virtual postcard web app (mobile-first) #1

Closed
opened 2026-06-15 15:03:41 +00:00 by mAi · 3 comments
Collaborator

What

A virtual-postcard web app. A sender composes a postcard (text message + images) and gets a unique shareable URL per recipient. Opening that URL renders the postcard (gallery or simple onepager layout). Mobile-first is a hard requirement, not an afterthought.

Provisional name postcards — final name/domain is still being chosen by m (candidates: grues.se (favourite), gruessdich.com, winkepost.de). Do not block the build on the domain and keep the repo/name easily renamable (no hardcoded domain strings — read base URL from env/config).

MVP scope

  1. Landing page — what it is + a clear "create a postcard" CTA. Mobile-first.
  2. Compose flow — write a text message + add images (upload). Mobile-friendly image picker/upload.
  3. Persist the postcard and generate a unique shareable URL per recipient (unguessable slug/token, not a sequential id).
  4. Render the postcard page from that URL — text + images, shown as a gallery or a clean onepager layout. Mobile-first.
  5. Sharing = shareable link first. Email delivery to the recipient is optional/later — out of MVP scope unless it falls out cheaply.

Stack (m's conventions)

  • Go server (primary language for servers).
  • Supabase: Postgres for postcard records, Supabase Storage for uploaded images. Reuse the existing shared Supabase instance (msupabase on mlake) with a dedicated postcards schema — match the pattern used by fdbck/flexsiebels. Do not stand up a new database.
  • Deploy via Dokploy on mlake (own Dokploy app/compose; Traefik domain wired later once m picks the name). Mirror the deploy shape of the other small web apps under ~/dev/web/.
  • Base URL / domain must come from config/env so a later rename is a one-line change.

Constraints / notes

  • Mobile-first, responsive, proper German Umlaute in any UI copy.
  • Unguessable recipient URLs (token-based). No auth required for recipients to view; sender flow can be open for MVP (no login) unless trivially addable.
  • Clean, sustainable code — no throwaway. README with run + deploy instructions.
  • Repo: m/postcards, local path ~/dev/web/postcards, sibling under ~/dev/web/ like fdbck.

Definition of done (MVP)

  • Landing → compose → persist → unique URL → render works end-to-end locally.
  • Images upload to Supabase Storage and render on the postcard page.
  • Mobile layout verified (landing, compose, postcard view).
  • Deployable via Dokploy (compose/app config committed); domain binding can follow.
  • Report back to mai/otto-wa when there's something showable so it can be relayed to m on his WhatsApp thread.

Kicked off via otto-wa delegation from m (WhatsApp).

## What A virtual-postcard web app. A sender composes a postcard (text message + images) and gets a **unique shareable URL per recipient**. Opening that URL renders the postcard (gallery or simple onepager layout). **Mobile-first is a hard requirement**, not an afterthought. Provisional name **postcards** — final name/domain is still being chosen by m (candidates: `grues.se` (favourite), `gruessdich.com`, `winkepost.de`). **Do not block the build on the domain** and keep the repo/name easily renamable (no hardcoded domain strings — read base URL from env/config). ## MVP scope 1. **Landing page** — what it is + a clear "create a postcard" CTA. Mobile-first. 2. **Compose flow** — write a text message + add images (upload). Mobile-friendly image picker/upload. 3. **Persist** the postcard and **generate a unique shareable URL** per recipient (unguessable slug/token, not a sequential id). 4. **Render the postcard page** from that URL — text + images, shown as a gallery or a clean onepager layout. Mobile-first. 5. **Sharing = shareable link first.** Email delivery to the recipient is optional/later — out of MVP scope unless it falls out cheaply. ## Stack (m's conventions) - **Go server** (primary language for servers). - **Supabase**: Postgres for postcard records, **Supabase Storage** for uploaded images. Reuse the existing shared Supabase instance (`msupabase` on mlake) with a dedicated `postcards` schema — match the pattern used by fdbck/flexsiebels. Do not stand up a new database. - **Deploy via Dokploy on mlake** (own Dokploy app/compose; Traefik domain wired later once m picks the name). Mirror the deploy shape of the other small web apps under `~/dev/web/`. - Base URL / domain must come from config/env so a later rename is a one-line change. ## Constraints / notes - Mobile-first, responsive, proper German Umlaute in any UI copy. - Unguessable recipient URLs (token-based). No auth required for recipients to view; sender flow can be open for MVP (no login) unless trivially addable. - Clean, sustainable code — no throwaway. README with run + deploy instructions. - Repo: `m/postcards`, local path `~/dev/web/postcards`, sibling under `~/dev/web/` like fdbck. ## Definition of done (MVP) - Landing → compose → persist → unique URL → render works end-to-end locally. - Images upload to Supabase Storage and render on the postcard page. - Mobile layout verified (landing, compose, postcard view). - Deployable via Dokploy (compose/app config committed); domain binding can follow. - Report back to `mai/otto-wa` when there's something showable so it can be relayed to m on his WhatsApp thread. Kicked off via otto-wa delegation from m (WhatsApp).
mAi self-assigned this 2026-06-15 15:03:50 +00:00
Author
Collaborator

MVP gebaut & verifiziert (shift-1)

Die virtuelle Postkarten-App ist end-to-end fertig und getestet.

Commit: d2b6dd0544
Branch: mai/hermes/build-mvp-virtual (push)

Flow (alle Punkte der DoD)

landing (/)  ->  compose (/compose)  ->  persist + Bild-Upload
             ->  redirect /c/{token}  ->  gerenderte Postkarte

Stack (nach m's Konventionen)

  • Go (stdlib net/http, Go-1.22-Router) — eine statische Binary, Templates + CSS via embed
  • Postgres auf der geteilten msupabase-Instanz, eigenes postcards-Schema (auto-migriert beim Start, idempotent)
  • Supabase Storage für Bilder (Bucket postcards, public; wird bei Bedarf automatisch angelegt). Zusätzlich local-Filesystem-Backend für die Entwicklung
  • Dokploy: Dockerfile (multi-stage) + docker-compose.yml committed; Traefik-Labels als Template (Domain folgt)

Wichtige Eigenschaften

  • Unguessable URLs: 128-bit crypto/rand → base64url-Token (kein sequentielles ID)
  • Renamable: keine hartcodierte Domain — BASE_URL aus env ist die einzige Stelle. Domain-Binding ist ein 1-Zeilen-Change (POSTCARDS_DOMAIN setzen + Traefik-Labels einkommentieren)
  • Mobile-first: Viewport, single-column Layout, responsive Galerie, Touch-freundlicher Bild-Picker mit Live-Vorschau
  • Deutsch mit korrekten Umlauten; compose/Postkarten-Seiten sind noindex
  • Validierung: leere Karte abgelehnt, nur JPEG/PNG/WebP/GIF, Größenlimit pro Bild

Verifiziert

  • End-to-end gegen die echte msupabase: Postkarte persistiert, Bild in Storage hochgeladen & abrufbar (200, image/png), Render-Seite zeigt Nachricht/Empfänger/Absender/Share-Link, 404 bei unbekanntem Token
  • Beide Storage-Backends (supabase + local) getestet
  • go build / go vet / go test grün (Unit-Tests für Token + Config)
  • docker build ok + Container-Smoke-Test (startet, migriert, /health 200)
  • Schema + Bucket in msupabase provisioniert; Test-Daten danach wieder gelöscht (Prod startet sauber, 0 Zeilen, leerer Bucket)

Offen (bewusst out-of-MVP / m's Entscheidung)

  • Domain (grues.se/gruessdich.com/winkepost.de) — Build nicht blockiert, jederzeit setzbar
  • Deploy auf mlake via Dokploy — Compose/Config liegt bereit; DATABASE_URL/SUPABASE_* in Dokploy-Env setzen, dann deploybar
  • Label-Taxonomie: Repo hat noch keine Labels — bewusst nicht eigenmächtig angelegt

Läuft. Wird an mai/otto-wa zur Weiterleitung an m gemeldet.

## ✅ MVP gebaut & verifiziert (shift-1) Die virtuelle Postkarten-App ist end-to-end fertig und getestet. **Commit:** https://mgit.msbls.de/m/postcards/commit/d2b6dd05447005dd6e8147fae0d2232442bae13f **Branch:** `mai/hermes/build-mvp-virtual` ([push](https://mgit.msbls.de/m/postcards/pulls/new/mai/hermes/build-mvp-virtual)) ### Flow (alle Punkte der DoD) ``` landing (/) -> compose (/compose) -> persist + Bild-Upload -> redirect /c/{token} -> gerenderte Postkarte ``` ### Stack (nach m's Konventionen) - **Go** (stdlib `net/http`, Go-1.22-Router) — eine statische Binary, Templates + CSS via `embed` - **Postgres** auf der geteilten **msupabase**-Instanz, eigenes `postcards`-Schema (auto-migriert beim Start, idempotent) - **Supabase Storage** für Bilder (Bucket `postcards`, public; wird bei Bedarf automatisch angelegt). Zusätzlich `local`-Filesystem-Backend für die Entwicklung - **Dokploy**: `Dockerfile` (multi-stage) + `docker-compose.yml` committed; Traefik-Labels als Template (Domain folgt) ### Wichtige Eigenschaften - **Unguessable URLs**: 128-bit `crypto/rand` → base64url-Token (kein sequentielles ID) - **Renamable**: keine hartcodierte Domain — `BASE_URL` aus env ist die einzige Stelle. Domain-Binding ist ein 1-Zeilen-Change (`POSTCARDS_DOMAIN` setzen + Traefik-Labels einkommentieren) - **Mobile-first**: Viewport, single-column Layout, responsive Galerie, Touch-freundlicher Bild-Picker mit Live-Vorschau - **Deutsch** mit korrekten Umlauten; `compose`/Postkarten-Seiten sind `noindex` - Validierung: leere Karte abgelehnt, nur JPEG/PNG/WebP/GIF, Größenlimit pro Bild ### Verifiziert - End-to-end gegen die echte **msupabase**: Postkarte persistiert, Bild in Storage hochgeladen & abrufbar (200, `image/png`), Render-Seite zeigt Nachricht/Empfänger/Absender/Share-Link, 404 bei unbekanntem Token - Beide Storage-Backends (supabase + local) getestet - `go build` / `go vet` / `go test` grün (Unit-Tests für Token + Config) - `docker build` ok + Container-Smoke-Test (startet, migriert, `/health` 200) - Schema + Bucket in msupabase provisioniert; **Test-Daten danach wieder gelöscht** (Prod startet sauber, 0 Zeilen, leerer Bucket) ### Offen (bewusst out-of-MVP / m's Entscheidung) - **Domain** (`grues.se`/`gruessdich.com`/`winkepost.de`) — Build nicht blockiert, jederzeit setzbar - **Deploy auf mlake** via Dokploy — Compose/Config liegt bereit; `DATABASE_URL`/`SUPABASE_*` in Dokploy-Env setzen, dann deploybar - Label-Taxonomie: Repo hat noch keine Labels — bewusst nicht eigenmächtig angelegt Läuft. Wird an `mai/otto-wa` zur Weiterleitung an m gemeldet.
Author
Collaborator

Merged to main and pushed: https://mgit.msbls.de/m/postcards/commit/8fb736c

Head review before merge: go build / go vet / go test all green from a clean checkout; token generation confirmed unguessable (128-bit crypto/rand, base64url). MVP is mergeable and complete per the DoD.

Not closing the issue (m closes). Open next steps remain m's call: final domain pick + Dokploy deploy (env secrets DATABASE_URL / SUPABASE_*).

Merged to `main` and pushed: https://mgit.msbls.de/m/postcards/commit/8fb736c Head review before merge: `go build` / `go vet` / `go test` all green from a clean checkout; token generation confirmed unguessable (128-bit `crypto/rand`, base64url). MVP is mergeable and complete per the DoD. Not closing the issue (m closes). Open next steps remain m's call: final domain pick + Dokploy deploy (env secrets `DATABASE_URL` / `SUPABASE_*`).
Author
Collaborator

🚀 Deployed & live: https://post.msbls.de

Deployed by otto-head per m's call (via otto-wa).

Setup: own Dokploy Application on mlake (appName postcards-msbls-de), Gitea source m/postcards@main, Dockerfile build. Domain post.msbls.de (wildcard *.msbls.de → mlake, Let's Encrypt cert). Wired to shared msupabase: postcards Postgres schema (DATABASE_URL → pooler 100.99.98.203:6789) + Supabase Storage bucket postcards via https://supa.flexsiebels.de. BASE_URL=https://post.msbls.de.

Verified end-to-end over HTTPS: landing 200 (valid cert) → compose (text + image upload) → persist → unguessable /c/{token} → rendered postcard with image from Supabase Storage; unknown token → 404. Test postcard cleaned up afterwards (0 rows, empty bucket).

Renamable: when m picks a final vanity domain, it's add-domain-in-Dokploy + set BASE_URL — no rebuild. post.msbls.de is the home until then.

One fix during deploy: first boot crash-looped — the postcards schema had been created under the supabase_admin role during shift-1 build-verification, so the app's postgres role lacked CREATE on it (migration runs every boot). Transferred schema + table ownership to postgres; clean since.

## 🚀 Deployed & live: https://post.msbls.de Deployed by otto-head per m's call (via otto-wa). **Setup:** own Dokploy **Application** on mlake (`appName postcards-msbls-de`), Gitea source `m/postcards@main`, Dockerfile build. Domain `post.msbls.de` (wildcard `*.msbls.de` → mlake, Let's Encrypt cert). Wired to shared **msupabase**: `postcards` Postgres schema (`DATABASE_URL` → pooler `100.99.98.203:6789`) + Supabase Storage bucket `postcards` via `https://supa.flexsiebels.de`. `BASE_URL=https://post.msbls.de`. **Verified end-to-end over HTTPS:** landing 200 (valid cert) → compose (text + image upload) → persist → unguessable `/c/{token}` → rendered postcard with image from Supabase Storage; unknown token → 404. Test postcard cleaned up afterwards (0 rows, empty bucket). **Renamable:** when m picks a final vanity domain, it's add-domain-in-Dokploy + set `BASE_URL` — no rebuild. `post.msbls.de` is the home until then. **One fix during deploy:** first boot crash-looped — the `postcards` schema had been created under the `supabase_admin` role during shift-1 build-verification, so the app's `postgres` role lacked CREATE on it (migration runs every boot). Transferred schema + table ownership to `postgres`; clean since.
mAi closed this issue 2026-06-23 15:48:01 +00:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/postcards#1
No description provided.