A unified directory, planner, and matchmaking surface for VJs, artists, and event spaces — built by the collective, for the collective. Closes the loops that today happen via DMs and tribal knowledge.
← Gooism CollectiveThe Gooism collective has three constituencies that today coordinate through DMs, group chats, and the kindness of mutual friends: VJs, Artists, and Event Spaces. Information is lossy at every hop. GPortal is the unified surface that closes those loops.
Booked by a friend-of-the-artist's-manager; arrives without knowing the rig, the audience, the artist's typical visual cues, or even the room's projector count.
Pinged with venue-tech questions they can't answer ("does the booth have a ground-loop hum?"). Ends up forwarding messages between strangers.
No way to verify a VJ's experience with a specific rig, headcount, or genre. Gambles each booking; sometimes wins, often regrets.
One tool, three faces of the same problem. Directory + Planner + Chat in a single surface, owned by the collective, with the data structured so every constituency benefits from the others' contributions. The Signal Planner reuses WallSpace's wall/output/scope data model so technical work flows directly between live shows and the planning surface.
GPortal lives under the Gooism collective umbrella, which is currently in a proposed structural phase (a 501(c)(3) is one of the options on the table, but nothing is decided — the Proposed Org Structure doc is the working draft, pending full collective approval + legal/financial advisor review). The accessibility-first mission is the through-line. Long-term home is gooism.org; short-term home is wallspace.studio/gooism/ while we build out the actual product surface at gportal.wallspace.studio.
Three first-class profile types. Each entity owns its own profile, sets its own discoverability rules, and edits its own fields. Cross-references resolve into hyperlinks (a VJ's "primary venue" jumps to that Space's profile; a Space's "house VJ" jumps to that VJ; etc.).
The visual artist running live shows. Ownership: the VJ. Edit gating: VJ + collective admins.
| Field | Type | Purpose |
|---|---|---|
| display_name | text | How they appear in directories |
| bio | text | Short narrative; AI-assist available (§13) |
| primary_venue_id | FK | Their home base, if any |
| experience_years | int | Career length |
| skills | json[] | e.g. ["controlnet", "midi-mapped", "scope-live", "milkdrop", "ndi-routing"] |
| tech_knowledge | json[] | e.g. ["resolume", "wallspace", "touchdesigner", "scope", "mainstage"] |
| venue_experiences | FK[] | List of spaces they've worked + show counts (drives "venue veterans" filter) |
| socials | json | Instagram, YouTube, TikTok handles |
| media_urls | R2[] | Reels + portfolio uploads |
| references_text | json[] | Public artistic references — "I'm influenced by" |
| rate_min / rate_max | int | Floor + typical ceiling, integer minor units |
| rate_currency / rate_unit | enum | USD + event/hour/night/day |
| rate_negotiable | bool | Willing to discuss outside the range |
VJs can publish 0+ named packages: pre-priced, scoped offerings that artists and spaces can purchase directly without going through the full custom-quote flow. This complements the per-event rate range — packages cover the cases where the offering is well-defined enough to put a price on. Lower buyer friction, faster to book, and lets specialists productize unique offerings.
Flagship example: Fetz's specialized CRT packages (4-unit / 8-unit / custom-cluster) ship as buyable line items, not "let's negotiate" inquiries.
| Field | Type | Purpose |
|---|---|---|
| name | text | e.g. "CRT Cluster — 4 unit / 4 hr" |
| category | enum | crt / projection / led / ai-live / recorded / audio-reactive / multi-cam / captions / consult / mentor / other |
| description | text | What this package is — the elevator pitch |
| price / currency | int / enum | Fixed price; minor units; USD default |
| duration_hours / duration_unit | real / enum | How long the package covers; unit is event / hour / set / license (for recorded) |
| max_event_size | int? | Sized for headcount up to N; null = any |
| includes_text | json[] | Bullet list — "4× consumer CRT, 1× video wall PC, cabling, setup + teardown, operator on-site" |
| requires_text | json[] | What the venue must provide — "2× 20A circuits, 8'×6' floor footprint, 2 h load-in" |
| addons_json | json | A la carte upsells: [{ name, price_delta, description }] (e.g. "+1 CRT", "Custom mounting", "Dual-operator") |
| lead_time_days | int | Minimum advance booking — physical packages need build/ship time |
| buyable_directly | bool | If true, artist/space can book without negotiation; if false, package is shown but routes through inquiry |
| travel_included_radius_km | int? | Free travel radius; null = no travel offered |
| travel_extra_per_km | int | Surcharge beyond the included radius |
| media_urls | R2[] | Example reels showing the package in action — matters most for visually-distinctive packages (CRT clusters, LED, projection) |
| sample_event_ids | FK[] | Past events_internal rows where this package was deployed — powers "see it live" cross-links |
| active | bool | Hide without deleting (seasonal packages, retired offerings) |
Package categories are not exhaustive — the other bucket is open. Initial curated list: CRT clusters, projection mapping, LED wall design, live AI / ControlNet restyling, recorded set rentals, audio-reactive performance, multi-camera live mix, caption / accessibility overlay packages, mentoring / training / consults.
Schema-level note: Artist and Event Space "packages" are a logical extension (artists offering set-length tiers, venues offering bundled night-rate-with-bar-split). Out of scope for v0.1; same pattern when added.
The musician / performer being booked. Two rate fields: their own performance fee, and the typical VJ-side spend when they're the booking party.
| Field | Type | Purpose |
|---|---|---|
| display_name | text | Stage name |
| genre | json[] | Multi-tag — e.g. ["techno", "ambient", "vocal-house"] |
| tech_rider_text | text | Free-form rider; structured fields planned for v1 |
| primary_vj_user_id | FK | Their go-to VJ when one exists |
| service_rate_min / max | int | The artist's own performance fee floor + ceiling |
| expected_vj_budget_min / max | int | What they typically allocate for visuals when booking |
| spotify_artist_id | text | Drives §09 preview embeds |
| soundcloud_url | text | Underground / pre-Spotify artists |
| bandcamp_url / apple_music_url | text | Additional embed surfaces |
| preview_track_url | text | "Listen to this first" — falls back via platform priority |
The venue. Capacity, rig, budget realities. Owned by an authorized account holder per venue.
| Field | Type | Purpose |
|---|---|---|
| display_name / city | text | Venue identity |
| capacity / max_screens | int | Headcount + max simultaneous outputs |
| layout_text | text | Free-form room description; structured layout schema in v1 |
| tech_capabilities | json[] | e.g. ["ndi", "syphon", "4k-projector", "5-crt-cluster", "led-wall-2x4m"] |
| skill_requirements | json[] | What a VJ needs to know to walk in cold |
| years_open | int | Track record context |
| house_vj_user_id | FK | The resident VJ when one exists |
| public_events | bool | Public-ticketed vs. private/corporate |
| vj_budget_min / max | int | Typical VJ-services budget per show |
| artist_budget_min / max | int | Typical artist-fee budget per show |
| budget_negotiable | bool | Door-open signal even when not in range |
Pay matching is the single most useful filter for both directions. A venue with a $300–500 VJ budget should not see a VJ whose floor is $800; a VJ should not waste pitches on under-budget venues. Storing min/max + currency + unit lets every search facet, the Event Planner, and the Hiring module reason about budget overlap without parsing free-text. Negotiable flags keep the door open when the situation calls for it without burying genuinely-incompatible matches.
Intermediaries and hiring agents who book talent + run nights at venues. Sit in the middle of the model: they have a "client roster" (talent they hire) AND a "venue partnership" list (where they run shows). Distinct from artists (talent themselves) and spaces (venues themselves). Example: Boof Party in SF runs nights at The Midway, hires Fetz + collective VJs directly.
| Field | Type | Notes |
|---|---|---|
| id | PK | nanoid |
| user_id | FK / nullable | Nullable — promoters often exist as a public handle before signing in. Admin-created entries are unowned until claimed. |
| display_name + slug + avatar_url | string | Same identity pattern as other directories |
| city / region | string | "Bay Area, CA"-style region grouping for non-city promoters |
| primary_contact_name | string | When the promoter is a brand (Boof Party) but the booking contact is a person |
| website_url + socials | JSON | Especially Instagram (the canonical promoter channel) |
| genre_focus | JSON array | What kinds of nights they run |
| typical_event_size | JSON array | intimate / medium / festival |
| partnered_venue_ids | JSON array | FK to event_space_profiles.id — venues they regularly run at. Powers cross-link on both sides. |
| typical_vj_budget_min / max | int | What they generally pay VJs — same matching role as venue's vj_budget fields |
| typical_artist_budget_min / max | int | What they generally pay artists |
| verified / booking_count / response_rate | trust | Collective-verified flag; derived counters populated as bookings flow through GPortal |
Some venues have multiple bookable rooms with different capacity / rig / vibe. The Midway (SF) has main concert hall (1500), Concourse (350), Patio (250), Annex Studio (150), and an opt-in Pier 80 expansion (5000). Each gig books one stage; the parent venue is the contact + billing surface.
Stored as stages_json on the parent event_space_profiles row — we don't need stages to be queryable as their own rows, just attachable to gigs / bookings. Shape:
[
{ "name": "Main Concert Hall", "capacity": 1500, "screens": 4 },
{ "name": "Concourse", "capacity": 350, "screens": 1 },
{ "name": "Patio", "capacity": 250 },
{ "name": "Annex Studio", "capacity": 150, "screens": 2 },
{ "name": "Pier 80 expansion", "capacity": 5000, "screens": 8,
"notes": "Opt-in. Requires permit + Midway crew." }
]
Three directory views — one per entity — sharing one underlying CRUD pattern. Rate-range is a first-class filter alongside skills, geography, availability, and genre. Cards show overlap state at a glance so out-of-band matches stay visible (the negotiation door is always cracked open) without burying in-band ones.
Every directory card shows the entity's rate range vs. the searcher's range:
Every directory hop is a hyperlink. A VJ's primary_venue chips to the Space profile. A Space's house_vj chips to the VJ profile. An Artist's primary_vj chips to the VJ. The graph is bidirectional: from a Space's profile, you can see "VJs who have worked here" pulled from the venue_experiences join table.
Fourth directory: a packages-only view that cuts across VJs. Filterable by:
Each package card shows the offering VJ's name + rating, the included scope, the price, and a "Book this package" CTA. Direct path from category browsing to confirmed booking, bypassing the multi-message custom-quote loop entirely when both parties are aligned.
The matchmaking + staffing + costing surface. Inputs go in (artist + space + date), reconciled requirements come out (tech list, VJ shortlist, cost estimate, rider/cost split). Confirms into a saved event everyone can see.
The planner's most distinctive surface. Side-by-side: venue VJ budget (min–max), shortlisted VJ rate ranges, artist fee, artist budget. Overlap state per pair shown as green/yellow/grey with a one-tap "negotiate" affordance that opens a thread pre-filled with the gap context. Same panel surfaces artist-fee vs. venue-artist-budget reconciliation.
A saved event record visible to all participants (VJ, artist, space, optional collective admins). Exportable as a one-pager that includes the agreed rate, payment terms, and rider/cost split. Once confirmed and the event date passes, ratings unlock for all parties (§08).
The planner's recommendations panel shows two parallel rails: Custom-fit VJs (the rate-matched, skill-matched shortlist for a bespoke quote) and Buyable packages (already-priced offerings that fit the event's needs). Spaces and artists who already know what they want can skip the negotiation entirely and book a package; the rest get the traditional shortlist. Same backend, two UX paths.
The technical design surface for an event — what's plugged into what, who runs which control, where each output lands. Visual node graph, not a form or spreadsheet. Signal flow is a graph; spreadsheets hide topology. Node graphs are how VJs already think.
This is a planning surface for the event, not a control surface for any one product. Resolume, TouchDesigner, OBS, VDMX, MadMapper, Notch, WallSpace, hardware-only rigs — all valid topologies. Apps are first-class nodes; you can mix multiple apps in one event (e.g. Resolume for live mix + ControlNet on a GPU pod + a Roland hardware video mixer for the final composite). WallSpace is one option among many, never assumed.
When an event has a confirmed venue, the planner pulls the venue's tech_capabilities in as the starting Output nodes, you wire your inputs/apps/processors into them, and the saved graph becomes a proposal — visible to the artist + space manager with comment threads, so the rig is locked before load-in instead of reverse-engineered on the night.
Camera, MP4, MIDI source, audio source, capture card, NDI, Syphon, AI input, Spotify-driven metadata.
Per-pixel effect, ControlNet preprocessor, AI model, scene mixer, audio analyzer, blend stage.
CRT, projector, NDI send, Syphon, WebRTC publish, file record, second-screen confidence monitor.
A human role — VJ, FOH, lighting tech — assigned to operate one or more processors / outputs. Powers the staffing surface in the Event Planner.
WallSpace's Wall / CRT / Projector / LayerState shapes (src/renderer/types/index.ts) translate directly to Output and Input nodes. v1.0+ supports import-from-WallSpace (existing setup JSON seeds the graph) and export-to-WallSpace (graph becomes a scene scaffold). Same compositor mental model on both sides of the surface.
Pre-built common graphs the collective curates. Each ships as a starting graph + a description of when to reach for it. Examples: 4-CRT cluster + camera + ControlNet, two-screen DJ wall + Spotify + caption layer, 3D scene + remote camera ingest, recorded-only fallback rig.
Implementation: @xyflow/react on the GPortal app. Graph stored as JSON in events_internal.signal_graph_json. v0.1 ships a static demo at /planner; v0.5 ships interactive create + save + load.
Three-tier sync: manual entry (always works), iCal feed read-only (lightweight), Google Calendar two-way (v1.0). Availability surfaces in directories with privacy gating: public busy/free, detailed view to confirmed connections only.
Drag-to-create on the calendar grid. Block + describe. Always available, no third-party setup. Lives in v0.5.
Drop a public iCal URL; GPortal polls and merges into the availability layer. v0.5 stretch.
OAuth, full read+write, conflict detection at booking time. Locks confirmed events back to Google as a held block. v1.0.
Recurring availability windows ("I'm available Thursday–Saturday nights, never Mondays") express as a separate layer that doesn't have to compete with one-off blocks.
Async-first; per-event group threads + DMs. Real-time presence is a v1.5 stretch goal. No public message walls in v1 — comms is targeted, not broadcast.
One-to-one threads between any two GPortal members. Emerges from a profile-page "Message" CTA or a Hiring/Outreach context.
Auto-created when an event confirms. All participants (artist, VJ, space owner, optional co-promoter) get added. The thread is the project-management surface.
Email + in-app. Per-event muting; per-thread muting. Quiet hours respected from the user's profile timezone.
Two-way: VJ ↔ Artist, VJ ↔ Space, Artist ↔ Space. Ratings only unlock for parties tied to a confirmed completed event in events_internal — no drive-by reviews, no review-bomb attack surface.
Star average + count. Optional one-line public "this person/space is solid" pull-quote per reviewer.
Long-form notes between the rated party and the rater. Visible to collective admins only on dispute.
Edge-case handling and arbitration policies are an open question for the collective — see §16.
Four external surfaces where GPortal pulls or interoperates: music platforms (Spotify + SoundCloud + Bandcamp + Apple Music), the 19hz event feed, Will's event.tools, and the Fetz intake form schema.
Every artist profile gets an embedded preview player so VJs and venues can sample an artist's actual sound before booking, and so artist research surfaces "what music am I designing visuals for?" in one click. Visual-design choices are genre-sensitive; today VJs Google an artist's Spotify after the booking locks. GPortal moves that step to before the conversation starts.
| Platform | Auth | What we get | Tier |
|---|---|---|---|
| Spotify | PKCE OAuth (reuse from WallSpace) | Latest album, top tracks, monthly listeners, follower count, official iframe player. TOS-compliant: display-only metadata, audio stays in Spotify domain. | v0.5 |
| SoundCloud | None — oEmbed | Public oEmbed widget (any track or profile URL). Metadata limited to title + thumbnail + artist name. Critical for underground / pre-Spotify artists. | v1.0 |
| Bandcamp | None — oEmbed | Embed widget for tracks + albums. Important for the experimental / DIY tier. | v1.5 |
| Apple Music | None — embed widget | music.apple.com /embed/ works without auth. Artist or album page. |
v1.5 |
Profile fields (per-artist, all nullable): spotify_artist_url, soundcloud_url, bandcamp_url, apple_music_url, plus a preview_track_url the artist designates as their "listen to this first" calling card. Player UI: small embed on directory cards (collapsed; click to expand-and-play), full-size embed on the Artist detail page, and an "Open in [platform]" deep-link as the always-available escape hatch.
19hz.info publishes plain CSV feeds at predictable URLs (https://19hz.info/events_<Region>.csv) with no auth and no API key — verified via the public phi-line/19hz.bot Discord bot scraping the same feeds. Available regions: BayArea, LosAngeles, Atlanta, Texas, Miami, Massachusetts. CSV columns: date, name, genre, location, time, price, ages, promoter, url1, url2, datetime.
events_external with source='19hz', region, and a content hash for change detection.location matches an event_space_profiles.display_name (fuzzy match), surface a "GPortal venue" badge on the imported event.events_internal.19hz covers underground West Coast electronic well, but a national, multi-genre footprint needs more sources. Below is the landscape we surveyed (per docs/SC5/vj_event_api_landscape.pdf), with the ingestion strategy GPortal will follow as we expand outward from the West Coast electronic feed.
| Source | API? | Best for | Strategy |
|---|---|---|---|
| 19hz.info | CSV feed (no auth) | Underground electronic, 6 US regions | Live in v0.5 Cron Worker pulls every 30 min; primary feed for early launch. |
| EDM Train | Public REST API | Plug-and-play electronic events nationwide | Core API. Add as second feed in v0.5; same events_external table with source='edmtrain'. |
| JamBase | API (developer key) | US live-music coverage; broad genre | Backup feed; fills gaps EDM Train misses (jam, jazz, indie, festivals). |
| Bandsintown | Artist-events API | Artist-side enrichment | Enrichment, not aggregation. When an artist profile lists a Bandsintown URL, pull their tour dates to seed availability + cross-reference incoming gigs. |
| Songkick | Enterprise API only | Strong global dataset | Defer until partnership / pricing makes sense. Not a v1 dependency. |
| Eventbrite | Public API | Mainstream + mixed-genre events | Noisy; ingest with strict filters (city + tags) only when a region requests it. Default off. |
| Resident Advisor | None — scrape | Underground / club / techno | Scraping layer (post-MVP) Critical dataset; queue-backed scraper, polite cadence, robots.txt-aware. Cache aggressively to minimize traffic. |
| Shotgun | None — scrape | Emerging promoter ecosystem | Same scraping pattern as RA. Surface promoter contacts into the lookup_cache for outreach. |
| Facebook Events | Restricted API | Underground event discovery | Leave for later — access barriers + ToS friction. Watch for policy changes. |
Ingestion architecture (v0.5+):
events_external table keyed by (source, source_id).events_external rows so we know "Artist X is playing this venue on Date Y."lookup_cache so outreach can target them directly.Future integration target. We want to hear what Will needs from GPortal before designing the interface. Listed here so it's not forgotten; not on the v0.1 critical path.
Reference doc: Fetz Artist/Event intake form. Used as a field-list reference when designing the Artist + Event intake schemas. No live integration; just structural inspiration.
Live-music bookings outpace visual-element bookings — most events run with simple lighting because no one pitched the artist or venue. GPortal turns 19hz from a passive feed into an active BD engine, handing VJs the right event + the right context + the right template at the right moment.
A research-aggregator usable on any artist, venue, or promoter — not just outreach contexts. Cached per target (14-day TTL) so repeated visits don't hammer external services.
Curated by the collective, customizable per VJ. Faceted by:
{{artist_name}}, {{event_name}}, {{venue}}, {{date}}, {{my_portfolio_url}}, {{my_recent_work}}, {{mutual}}Template versioning + anonymized A/B success-rate tracking ("Template Y replied 38% of cold emails to BayArea venues") so the collective improves the library over time.
Per-VJ. Per-target row with channel + template + sent_at + status + response_at + notes + follow-up reminder. Filterable by target type / status / date range / event. CSV export for VJs who keep their own pipeline tools.
Outreach log is private to the sender + collective admins. Recipients (artists, venues) never see GPortal-internal notes or status. Public "X has worked with Y" badges only after a confirmed booking — never from outreach alone. VJs can opt out of leaderboards / aggregate stats while still using the tool.
The inverse direction: artists and event spaces actively hiring VJ talent. Pairs with §10 to make GPortal a fully bidirectional matching engine.
Today, a venue that wants visuals usually has to route the conversation through the artist's manager → artist → "do you know a VJ?" → friend-of-friend → eventually a VJ. Information loses fidelity at every hop — the venue's actual capability spec, the date, the budget, the rig.
GPortal collapses this. A venue (or artist) can directly look up VJs by availability + skill + venue experience + rate compatibility, request a quote, exchange the missing context inline, and move into project management — all in one thread, with no third-party hand-offs. Same path works in reverse for VJs reaching artists. The artist stops being an unwilling middleman; the venue stops getting under-qualified VJs; the VJ stops getting incomplete briefs.
"Looking for a VJ" — posted by an artist or event space; visible to all VJs; multiple applicants compete; lister picks one.
Browse VJ directory → click profile → "Request booking" → structured inquiry lands in the VJ's inbox.
Browse packages directory or a VJ's profile → click "Book this package" → structured purchase with the price, scope, and lead time pre-defined. Skips the multi-message negotiation when the package fits.
addons_json), travel surcharge calculated automatically based on venue location, total price shown.buyable_directly=true, the package goes straight to the VJ's inbox as booking_requests.status='pending' with a package_id set; the VJ accepts (auto-confirms event) or counters (returns to negotiation flow).buyable_directly=false, the package surfaces but routes through standard inquiry — the VJ wants to vet fit before committing.events_internal with the package snapshot frozen on the event record (so future package edits don't change historical bookings).events_internal record with primary_vj_id set; remaining applicants get a courtesy decline.pending.events_internal and locks the date in the VJ's calendar.A venue or artist posts a gig with target=collective instead of choosing a specific VJ. The collective coordinator (or an automated rotation) routes it to the best-fit available VJ. Reduces the search burden on first-time clients.
Non-profit events — community shows, accessibility-focused performances, educational outreach, benefits — should be able to pursue grant funding and sponsorship without leaving GPortal. Dovetails with Gooism's potential 501(c)(3) status (proposed; pending collective + legal review — see Proposed Org Structure) and the explicit accessibility-for-visual-art mission.
VJs and event planners regularly want to fund community-focused work but lack the legal vehicle, the grant-database knowledge, and the time to draft applications. Embedding the workflow next to event planning means a single thread — "this is the event, this is the budget, here are the grants that match, here's the draft application" — instead of three disconnected tools.
Curated DB by category: visual + digital arts; music / live performance; accessibility (deaf / hard-of-hearing — direct Gooism mission); community + cultural; local / regional; foundation; corporate sponsorship; crowdfunding. Each entry: eligibility, award range, deadline cadence, applicant-type requirements, past-success notes.
For an events_internal flagged non_profit=true, ranks grants by eligibility match, award size relative to budget gap, deadline feasibility (most arts grants want 3–6 months lead time — hard-filter anything that can't make the deadline), past-success rate within the collective.
Templates + AI-assisted (Claude API) drafting: letter of intent, project narrative, budget narrative (auto-populated from Event Planner), outcomes framework, boilerplate for Gooism collective + applicant-entity status (pending Proposed Org Structure review) + accessibility mission, references to past grant-funded events.
Same workflow as VJ Outreach (§10) but targeting local businesses (cash + in-kind asks: gear loan, venue donation, hospitality), corporate sponsorship programs, crowdfunding kick-off (Kickstarter / GoFundMe / Patreon scaffolding).
Per-event funding pipeline: applied / under review / awarded / declined / withdrawn. Stacking visualization (multiple grants against one event budget). Cash-flow timeline (when each award disburses). Match-funding tracking (some grants require 1:1 match).
Required reporting cadence (interim + final), documentation requirements (photos, attendance, demographics, accessibility metrics), final report templates pre-populated from event metadata, reminder cadence so reports don't slip.
Events that include captioning / sign interpretation / hearing-loop accessibility get a dedicated grant filter — accessibility-specific funders (e.g. NEA Accessibility, regional Deaf-arts foundations, ADA-compliance grants) often fund things general-arts grants won't. The collective's existing A.EYE.ECHO + Matt-led accessibility work makes this a credible applicant story.
Modeled on the gifting app's existing "opus generate" pattern. One Claude-API surface inside GPortal, surfaced contextually wherever drafting, personalizing, or triaging is the bottleneck. Every AI-touched output is logged with ai_assisted=true for transparency, and the user always reviews + edits before anything sends.
/api/ai/generate accepts { surface, payload, model, max_tokens }.@anthropic-ai/sdk) with claude-opus-4-7 for high-stakes drafts (outreach, grants, bios) and claude-sonnet-4-6 for shorter / faster surfaces.src/lib/prompts/<surface>.ts.Every AI-generated draft has a visible "AI assisted — edit before sending" banner. The final stored body is always the edited version the user actually sent, not the raw AI output — this avoids inflating success-rate metrics on AI drafts that humans heavily rewrote. Aggregate analytics ("AI-assisted drafts replied at X% vs. manual at Y%") published to the collective for honest evaluation; per-VJ usage stays private.
Funder-facing surfaces (grant applications) carry an explicit AI-disclosure flag — some funders prohibit AI-generated narratives, and we surface the rule in the directory entry for that grant.
Four tiers, additive. v0.1 is the spec doc + skeleton. Each tier is shippable on its own; we don't gate on the next one.
Reuse over rebuild. The app sits on Cloudflare's stack (already provisioned for wallspace.studio); auth carries over via cross-subdomain cookies; UI primitives port from the gifting app; the node editor is the one new dependency.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) | Mirrors the gifting app; React-first which we need for xyflow; Server Components for DB-backed list views; Server Actions for forms. |
| Hosting | Cloudflare Pages + OpenNext | @opennextjs/cloudflare; same deploy paradigm as gifting; subdomain gportal.wallspace.studio for v0.1. |
| UI primitives | shadcn/ui + Radix + Tailwind | Port from gifting src/components/ui/. Battle-tested form / dialog / tabs / dropdown. |
| Node editor | @xyflow/react (MIT) | De-facto standard React node-graph library. Beats rete.js (more boilerplate), drawflow (vanilla but ugly), and roll-your-own. |
| Auth | Reuse wallspace.studio JWT + Google/GitHub/Apple OAuth | Already wired. Cross-subdomain cookies via Domain=.wallspace.studio. A wallspace account is a GPortal account. |
| DB — identity | Existing AUTH_DB D1 binding | Users table already lives there. We just FK user_id. |
| DB — GPortal data | New GPORTAL_DB D1 binding | VJ / Artist / Space profiles, events, hiring, outreach, grants. Independent schema evolution. |
| Media storage | Existing MEDIA_BUCKET R2 | Reuse signed-upload pattern from assets/web-functions/api/upload.ts. |
| AI | Anthropic SDK + prompt caching | Claude Opus 4.7 / Sonnet 4.6. Port pattern from gifting's ai-provider.ts. |
| Music | Spotify Web API + SoundCloud/Bandcamp/Apple oEmbed | Spotify port from WallSpace; everything else needs no auth. |
| 19hz feed | Cloudflare Cron Worker + CSV parse | Public CSV; 30-min schedule; content-hash diff. |
| Resend (port from gifting) | For transactional notifications + decline / accept / reminder emails. |
Decisions GPortal can't make alone. These should land at office hours so the schema + UX commit cleanly.