← back to NationalPaperHangers
yolo tick 1: paper_threads + paper_comments schema + 5 seed threads
4dca72eb815ffeaefd36179e770ea44489cb42ee · 2026-05-10 17:09:30 -0700 · SteveStudio2
Migration 018 lands the schema for UX backlog #3 ("This Paper" peer-
installer commentary). Schema only — no public routes yet, so the
tables are inert and the feature isn't visible to users.
paper_threads (id, slug, brand, paper_name, sku, paste_type, category,
description, seeded_by, comment_count, timestamps). 5 seed rows for
the most-commented papers in luxury wallcovering: de Gournay Earlham,
Fromental Bois, Phillip Jeffries Manila Hemp, Maya Romanoff Ajiro,
Schumacher Vassily.
paper_comments (thread_id, installer_id, body, helpful_count, flagged,
timestamps) — public-readable, installer-only writable via existing
requireInstaller middleware (wired in next tick).
Trigger paper_threads_recount() keeps comment_count denormalized so
sort-by-activity is cheap.
Next tick: public /papers list + /papers/:slug detail (read-only).
Files touched
A db/migrations/018_paper_threads.sql
Diff
commit 4dca72eb815ffeaefd36179e770ea44489cb42ee
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Sun May 10 17:09:30 2026 -0700
yolo tick 1: paper_threads + paper_comments schema + 5 seed threads
Migration 018 lands the schema for UX backlog #3 ("This Paper" peer-
installer commentary). Schema only — no public routes yet, so the
tables are inert and the feature isn't visible to users.
paper_threads (id, slug, brand, paper_name, sku, paste_type, category,
description, seeded_by, comment_count, timestamps). 5 seed rows for
the most-commented papers in luxury wallcovering: de Gournay Earlham,
Fromental Bois, Phillip Jeffries Manila Hemp, Maya Romanoff Ajiro,
Schumacher Vassily.
paper_comments (thread_id, installer_id, body, helpful_count, flagged,
timestamps) — public-readable, installer-only writable via existing
requireInstaller middleware (wired in next tick).
Trigger paper_threads_recount() keeps comment_count denormalized so
sort-by-activity is cheap.
Next tick: public /papers list + /papers/:slug detail (read-only).
---
db/migrations/018_paper_threads.sql | 110 ++++++++++++++++++++++++++++++++++++
1 file changed, 110 insertions(+)
diff --git a/db/migrations/018_paper_threads.sql b/db/migrations/018_paper_threads.sql
new file mode 100644
index 0000000..425d06e
--- /dev/null
+++ b/db/migrations/018_paper_threads.sql
@@ -0,0 +1,110 @@
+-- 018 · "This Paper" peer-installer commentary (UX idea #3)
+--
+-- Tribal wallcovering knowledge — install quirks per paper (drop %, paste
+-- type, soak time, seam matching) — lives in 3,000 working installers' heads
+-- worldwide, in private Slacks and IG DMs. Make it the public-facing
+-- infrastructure of the directory and capture the whole surface.
+--
+-- Threads are seeded by ops + claimed by craft (any installer can add a new
+-- paper). Comments are PUBLIC-READABLE but only verified installer members
+-- can WRITE — keeps the conversation craft-quality, gives designers a
+-- selection signal (which installers contribute), and makes the directory
+-- the canonical reference for "how do you install <paper>."
+--
+-- Reversible:
+-- DROP TABLE paper_comments;
+-- DROP TABLE paper_threads;
+
+BEGIN;
+
+CREATE TABLE IF NOT EXISTS paper_threads (
+ id SERIAL PRIMARY KEY,
+ slug TEXT NOT NULL UNIQUE,
+ -- url-safe identifier — generated from brand + paper_name
+ -- e.g. "de-gournay-earlham", "fromental-bois", "maya-romanoff-ajiro"
+ brand TEXT NOT NULL,
+ paper_name TEXT NOT NULL,
+ sku TEXT,
+ -- manufacturer SKU if known — multiple SKUs may map to one thread
+ -- (different colorways of the same paper share install quirks)
+ paste_type TEXT,
+ -- typical paste recommendation — strippable | clay | wheat | varies
+ category TEXT,
+ -- silk | grasscloth | chinoiserie | mural | metallic | flock | foil | embroidered | varies
+ description TEXT,
+ -- 1-2 paragraph intro to the paper, written by ops or by the
+ -- first contributing installer. Edited only by ops + admins.
+ seeded_by TEXT,
+ -- nph_ops | <installer_slug> — provenance for the thread
+ comment_count INTEGER NOT NULL DEFAULT 0,
+ -- denormalized counter, incremented on insert into paper_comments
+ -- (kept fresh by trigger below)
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX IF NOT EXISTS idx_paper_threads_brand ON paper_threads(brand, paper_name);
+CREATE INDEX IF NOT EXISTS idx_paper_threads_category ON paper_threads(category);
+CREATE INDEX IF NOT EXISTS idx_paper_threads_count ON paper_threads(comment_count DESC);
+
+CREATE TABLE IF NOT EXISTS paper_comments (
+ id SERIAL PRIMARY KEY,
+ thread_id INTEGER NOT NULL REFERENCES paper_threads(id) ON DELETE CASCADE,
+ installer_id INTEGER NOT NULL REFERENCES installers(id) ON DELETE CASCADE,
+ -- installer who wrote it — gates write via requireInstaller +
+ -- (later) installer_members.role IN ('owner','member')
+ body TEXT NOT NULL,
+ helpful_count INTEGER NOT NULL DEFAULT 0,
+ flagged BOOLEAN NOT NULL DEFAULT false,
+ -- ops-flagged for review — hidden from public
+ edited_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX IF NOT EXISTS idx_paper_comments_thread ON paper_comments(thread_id, created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_paper_comments_installer ON paper_comments(installer_id, created_at DESC);
+
+-- Keep comment_count fresh — light-weight trigger.
+CREATE OR REPLACE FUNCTION paper_threads_recount() RETURNS trigger AS $$
+BEGIN
+ IF TG_OP = 'INSERT' THEN
+ UPDATE paper_threads SET comment_count = comment_count + 1,
+ updated_at = now()
+ WHERE id = NEW.thread_id;
+ ELSIF TG_OP = 'DELETE' THEN
+ UPDATE paper_threads SET comment_count = GREATEST(0, comment_count - 1),
+ updated_at = now()
+ WHERE id = OLD.thread_id;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS trg_paper_comments_count ON paper_comments;
+CREATE TRIGGER trg_paper_comments_count
+ AFTER INSERT OR DELETE ON paper_comments
+ FOR EACH ROW EXECUTE FUNCTION paper_threads_recount();
+
+-- Seed 5 example threads — canonical luxury wallcovering papers that almost
+-- every working installer has an opinion on. These give the section
+-- immediate content on launch; the description copy is short and factual
+-- so it doesn't pre-empt installer contributions.
+INSERT INTO paper_threads (slug, brand, paper_name, paste_type, category, description, seeded_by) VALUES
+ ('de-gournay-earlham', 'de Gournay', 'Earlham', 'wheat', 'chinoiserie',
+ 'Hand-painted chinoiserie on hand-dyed silk. Panel-based (not roll-based) — each panel comes pre-cut to ceiling height. Wheat paste applied to the wall, not the paper. Soak-time is critical: too short and the silk slips during alignment; too long and the ink bleeds. Standard panel width ~91 cm.',
+ 'nph_ops'),
+ ('fromental-bois', 'Fromental', 'Bois', 'clay', 'chinoiserie',
+ 'Hand-painted on silk with embroidered detail elements on select skus. Like de Gournay, sold as panels — but Fromental panels accept clay paste better than wheat in humid environments. Embroidery sections require a soft-roller pass with light pressure during the dry-down phase.',
+ 'nph_ops'),
+ ('phillip-jeffries-manila-hemp', 'Phillip Jeffries', 'Manila Hemp', 'clay', 'grasscloth',
+ 'The benchmark grasscloth — natural variation between panels is the design, not the defect. Reverse-hang every other strip to balance shade variation. Clay paste is mandatory; vinyl paste causes staining. Trim selvedge with a straightedge — pre-trimmed bolts are rare and inconsistent.',
+ 'nph_ops'),
+ ('maya-romanoff-ajiro', 'Maya Romanoff', 'Ajiro', 'strippable', 'specialty',
+ 'Origami-folded wood veneer on cloth backing. The fold pattern means every cut must be measured TWICE — there is no "average" repeat. Strippable paste only; clay attacks the veneer adhesive. Practice on a sacrificial panel before going to the wall.',
+ 'nph_ops'),
+ ('schumacher-vassily', 'Schumacher', 'Vassily', 'clay', 'metallic',
+ 'Metallic ground with screen-printed pattern. Two installer pitfalls: (1) the metallic ground reads differently under warm vs. cool light — always confirm orientation matches the installer-supplied sample; (2) clay paste smudges easily, so wipe seams immediately and never reuse the wiping cloth.',
+ 'nph_ops')
+ON CONFLICT (slug) DO NOTHING;
+
+COMMIT;
← b681b43 UX_CREATIVE_BACKLOG: mark #5 (Live COI) shipped v0
·
back to NationalPaperHangers
·
yolo tick 2: public /papers list + /papers/:slug detail (rea 4492052 →