Hoppa till innehåll

Fallstudie

Coyotes and Candles: Att bygga upp en kreativ helhetsplattform för ett tvåmansföretag

Roll: Fristående fullstack-utvecklare och medgrundareVaraktighet: Juni 2026 (cirka 8 dagars aktiv utveckling)Status: Öppet och tar emot bokningar
Next.js 16SupabasePostgreSQLStripePayPalAgoraUpCloud + pm2

Skala

501

Commits

~65,700

TypeScript-rader

93

Sidrutter

74

API-rutter

65

SQL-schemafiler

5

Ersatta betalverktyg

Problemet

Min fru Alli och jag driver Coyotes and Candles - ett tvåmansföretag som erbjuder tarotspådomar, D&D-engångsspel, privata gruppkampanjer och en online-community. Innan vi byggde upp det här systemet hanterades varje del av verksamheten i olika verktyg:

  • -Calendly för tidsbokning (ingen betalning, ingen videolänk, ingen uppföljning)
  • -Zooma för video (manuella länkar, inget inbyggt sammanhang)
  • -Patreon och Ko-fi för prenumerationer (spridda målgrupper)
  • -Discord för gemenskapen (separat från allt annat)

Kunderna var tvungna att hoppa mellan fem olika verktyg för att boka en session, delta i ett videosamtal och hitta communityn. Vi behövde en enda plattform som kunde hantera allt från bokning till uppföljning efter sessionen.

Vad som byggdes

En integrerad plattform med sex olika bokningsflöden, två betalningsleverantörer, inbyggd livevideo, en fullt utrustad virtuell bordsskiva, ett community-system och en administratörspanel - som ersätter alla fem verktygen.

Bokning och betalning

  • ·6 bokningsflöden: tarotspådomar, engångsbesök, privata grupper, kampanjer med uppskjuten fakturering, presentkort, prenumerationer
  • ·Stripe för engångsbetalningar och återkommande prenumerationer
  • ·PayPal Subscriptions API med fullständig hantering av webhook-livscykeln
  • ·Över 20 automatiserade e-postmallar: bekräftelser, påminnelser, felmeddelanden och återställning
  • ·Tillgänglighetskalender i realtid med hänsyn till tidszoner
  • ·Automatisk återbetalning vid avbokning minst 48 timmar före sessionen
  • ·Självbetjäning för ombokning och platsbokning för kampanjer med uppskjuten betalning
  • ·Presentkort med kampanjkoder

Sessionshantering (VTT)

  • ·Inbyggda Agora-rum för livevideo - inga nedladdningar krävs
  • ·Helt virtuellt spelbord: spelpjäser, krigsdimma, frihandsteckning, tärningskastare
  • ·D&D 5e-karaktärsblad med fullständig integration av SRD-kompendiet
  • ·Karaktärsblad för Shadowrun 4e med import av Chummer XML
  • ·GM-styrd musik via YouTube
  • ·CoyoteCloud: realtidsuppdatering och dubbelriktad synkronisering med DiceCloud, så att ändringar av karaktärer i DiceCloud omedelbart visas i VTT

Gemenskap

  • ·Kanalsystem i Discord-stil, kanaler med kampanjbegränsad åtkomst
  • ·Direktmeddelanden, reaktioner, skrivindikatorer
  • ·Webb-pushmeddelanden
  • ·Moderationsverktyg och granskningsloggar
  • ·Valfri spegling av Discord-webhook

Administratörspanelen

  • ·Bokning och kampanjhantering
  • ·Spelarhantering med anpassade priser och närvaroregistrering
  • ·Anteckningar och sammanfattningar från sessionerna
  • ·Intäktsanalys och resultaträkningsöversikt
  • ·Integration med Google Search Console
  • ·Felövervakning med datareducering

Skärmdumpar

Teknisk arkitektur

FrontendNext.js 16 (App Router), TypeScript, Tailwind CSS
BackendSupabase (PostgreSQL, Auth, Realtime) with Row Level Security
DeploymentUpCloud VPS via pm2 + Caddy reverse proxy
PaymentsStripe (one-time + recurring), PayPal Subscriptions API
VideoAgora SDK - embedded rooms, no downloads
SecurityNonce-baserad CSP, atomära databasoperationer, tidssäkra jämförelser
ComplianceGDPR-anpassad, finsk moms (25,5 %), strukturerade data i JSON-LD, finsk enskild näringsidkare (Y-nummer: 3572436-7)

Vad jag lärde mig

Det här är vårt eget företag, så återkopplingsvägen är kort: om något går snett i bokningsprocessen får Alli reda på det av kunden redan samma kväll. Det mesta av det som följer finns med i ändringsloggen eftersom det först gick fel.

En kontroll som inte skiljer en förnyelse från en dubblett stoppar förnyelsen

Långa videosamtal avbröts efter nästan exakt fyra timmar. Agoras åtkomsttoken upphör då att gälla, och klienten förnyar dem genom att återigen gå via tokenrutten, men den rutten innehöll en kontroll av typen ”redan i denna session” och den uppringandes egen aktiva hjärtslagssignal utlöste den. Förnyelsen returnerade ett 409-fel och misslyckades utan att det märktes, så Agora kopplade bort dem mitt i sessionen. Lösningen var att låta klienten markera en begäran som en förnyelse, vilket gör att dubbelkontrollen hoppas över, samtidigt som en äkta andra anslutning fortfarande avvisas. Det är värt att komma ihåg den allmänna principen: varje kontrollmekanism som identifierar uppringare utifrån deras närvaro kommer så småningom att stöta på den legitima uppringaren två gånger.

En UPDATE som inte matchar några rader är inte ett fel

Nya mötesrum glömde tyst bort alla inställningar: rollformulärssystem, standardinställningar för rutnätet, läge. I den driftsatta tabellen saknades en kolumn som insättningen skrev till, så det gick inte att skapa konfigurationsraden, och varje senare uppdatering matchade då noll rader och rapporterades som lyckad, eftersom det är ett fullt giltigt resultat i SQL att inte uppdatera någonting. Äldre rum fungerade utan problem, eftersom de skapades innan kolumnen fanns. Inget i stacken skulle av sig själv upptäcka detta. En skrivväg måste kontrollera att den faktiskt har ändrat något, annars förblir felet osynligt tills någon påpekar att deras inställningar aldrig sparas.

En blockerad begäran ser ut som en bugg, inte som en blockering

Twitch-chattens överlägg visades perfekt, inklusive rubriken, och visade aldrig en enda rad. I CSP:s connect-src-fält angavs Supabase, Agora och Stripe, men inte Twitchs IRC-WebSocket, så webbläsaren avvisade anslutningen innan den hunnit öppnas. Samma problem uppstod ytterligare två gånger: next/image vägrade tyst att rendera banner-URL:er som administratören lagt in och vars värdar inte fanns med på tillåtelselistan, och ett partnerprojekt skärpte sin säkerhet på radnivå, vilket skulle ha förstört en import som hittills bara fungerat eftersom tabellerna var läsbara för alla. Tillåtelselistor misslyckas diskret och i det tysta, och symptomet framträder alltid som att din egen funktion inte fungerar.

Kontrollera-sedan-skriv är en kapplöpningssituation; låt databasen avgöra

Koder för presentkort genererades genom att först kontrollera om en kod redan fanns och sedan infoga den, vilket sker från kontrolltidpunkten till användningstidpunkten och därmed så småningom leder till kollisioner. Detta ersattes av en infogning som gör ett nytt försök vid brott mot unikhetsbegränsningen, vilket gör databasen till den som avgör istället för applikationen. Samma resonemang gäller när Stripe gör ett nytt försök med en webhook: dubbla fakturahändelser hoppas nu över med hjälp av en idempotensnyckel istället för att man ska behöva hoppas på att de aldrig dyker upp två gånger. Allt som har en begränsad tillgång eller en ”högst en gång”-effekt kräver begränsningen, inte kontrollen.

Skilj ”finns det här” från ”får du se det”

Efter en nyckelrotation i Supabase fick medlemmar som inte hade loggat in sedan dess meddelandet ”den här sessionen är inte tillgänglig” när de försökte ansluta till ett samtal. Deras lagrade token var signerad med den gamla nyckeln, vilket ledde till att läsningen med säkerhet på radnivå returnerade noll rader. Routen drog därför slutsatsen att rummet inte existerade, och gränssnittet visade 404-felet som en saknad session. Rummet fungerade som det skulle; meddelandet var felaktigt. Nu sker existenssökningar via serviceklienten medan autentiseringen kontrolleras separat, så ett inaktuellt token ger ett tydligt 401-fel och en inloggningsuppmaning. Att blanda ihop dessa två frågor försvagar inte bara auktoriseringen, det gör också att dina felmeddelanden blir felaktiga.

Länkar