production

$ cat system.md

For the curious. The full technical colophon.

triplink — stats
$ triplink --stats
55
prisma.models
110
api.routes
288
test.assertions
73
app.pages
207
react.components

dependencies

framework

  • next15.x
    App Router, RSC, streaming
  • react19.x
    Server components default
  • typescriptstrict
    Zero any policy

data

  • postgresqlNeon
    Serverless, branching
  • prismaORM
    55 models, typed queries
  • upstash/redisREST
    Rate limits, cache

auth

  • clerk5.x
    SSO, orgs, RBAC

payments

  • stripeSDK
    Checkout, webhooks, subscriptions, metered billing

ai

  • openaiGPT-4o
    Email parsing, trip assistant, NL commands

messaging

  • twilioSDK
    SMS + WhatsApp
  • resendAPI
    Transactional email
  • @slack/sdklatest
    Bot + slash commands

observability

  • sentrySDK
    Error tracking, tracing
  • vercel/insights
    Web Vitals, speed
  • ga4
    Funnels, retention

infra

  • vercelEdge + Node
    Zero-config deploys
  • serwist9.x
    PWA, offline-first

ui

  • tailwindcssv4
    Utility-first
  • lucideicons
    Tree-shakeable
  • cmdk
    Command palette (Cmd+K)
  • dnd-kit
    Drag & drop itinerary
  • sonner
    Toast notifications

maps

  • leafletOSM
    Interactive maps, no Google tax

testing

  • vitestunit
    Fast, ESM-native
  • playwrighte2e
    Cross-browser
  • storybook8.x
    Component isolation

architecture

architecture.ts
1
render: 'server'

Server components by default. Client boundaries only where you click, type, or drag.

2
cache: TanStack Query

Client-side cache with optimistic updates. Mutations feel instant.

3
rateLimit: Upstash

Redis-backed rate limiting on all API routes. Sliding window algorithm.

4
offline: Serwist

Service worker pre-caches the app shell. View your itinerary on a plane.

5
partner: white-label

B2B partner system. Custom branding, subdomain routing, org-scoped data.

6
notify: multi-channel

Same event fires to email, SMS, WhatsApp, Slack, and in-app. User picks.

features --verbose

AI assistant parses natural language into structured actions

"Split the Airbnb 4 ways" → creates expense, assigns members, calculates shares

Email forwarding parser

Forward a booking confirmation → AI extracts dates, location, confirmation # → adds to itinerary

QR code invite links

Scan to join. No app download. Deep links to the right trip.

Calendar export (ICS)

Subscribe URL auto-updates when the itinerary changes

PDF itinerary export

Print-friendly. Includes maps, times, addresses, notes.

Flight status tracking

Real-time delay/gate change alerts pushed to the group

Real-time collaboration

Optimistic UI. Multiple people editing the same trip, no conflicts.

pricing --explain

tier_config.ts
$ triplink tiers --format table
tiertripsmemberscreditsitineraryexpensesprice
FREE31010/mo5/day20/trip$0
PRO50100/mo$4.99/mo
TEAM500/mo$9.99/mo

permission piggybacking

Feature access is checked per-trip, not per-user. When a Pro organizer invites free users, those users get Pro features for that trip only. Free users on their own trips still get free limits.

Every invite is a free trial. Friends experience Pro features without paying. When they want to organize their own trip, they upgrade.

AI credits piggyback too. On Team plans, members spend from the organizer's credit budget instead of their own.

Conversion happens at organizer-time. Free users convert when they want to create and run their own trip — not when they're a guest.

// Limits enforced server-side per API route. Never trust the client.

// Every feature ships to Free. Paid tiers unlock scale, not features.

// Stripe handles checkout, webhooks, subscription lifecycle. No custom billing code.

ai.credits

ai-budget.ts
cost_per_action:1 credit

Each AI message costs exactly 1 credit

model:gpt-4o-mini

Fast, cheap, good enough for trip ops

rate_limit:50/hr, 200/day

Per member. Sliding window.

max_tokens:4,000

Per message. Keeps responses focused.

concurrency:3

Max simultaneous conversations per member

piggybacking:TEAM owners

Members use the trip owner's credit budget on TEAM plans

cost_tracking:per message

Input/output tokens tracked. Total cost stored per conversation.

config:runtime

Rate limits configurable via AIConfig table. No redeploy needed.

expenses.split()

EQUAL
$10.00 ÷ 3 = [$3.34, $3.33, $3.33]

Cent-based integer arithmetic. Penny remainder distributed round-robin.

PERCENTAGE
60/30/10 of $100 = [$60, $30, $10]

Extra pennies go to members with largest fractional parts.

ITEMIZED
Item A → [Alice, Bob], Item B → [Alice]

Per-item equal splits, aggregated across all items.

EXACT
Alice: $45, Bob: $30, Carol: $25

Specify exact amounts. Must sum to total.

settlement.optimize()

Greedy algorithm matches largest debtors to largest creditors. Minimizes total transactions. Sub-cent epsilon tolerance (0.005) handles rounding.

notify.fanout()

notifications — channels
$ triplink notify --list-channels
in-app
Instant

Notification model with type enum (8 types), linkPath for deep linking, metadata JSON

slack
Real-time

Rich block messages with action buttons. Per-channel toggles: expenses, polls, itinerary, members

whatsapp
Real-time + daily

Plain text with markdown. Daily summary digest at configurable time. Trip countdown.

email
Transactional

Resend API. Booking confirmation parser on inbound webhook.

sms
On-demand

Twilio. Used for critical alerts (flight delays, gate changes).

// Same event triggers all channels. Idempotency guards prevent duplicate delivery per member per day.

security

Ratelimit.slidingWindow(60, "10s")

Upstash Redis. 60 requests per 10 seconds per IP. Returns X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers. 429 on breach.

clerkMiddleware()

Every protected route requires valid Clerk session. Permission checks: requireMember(tripId, userId) + requireFeatureAccess(userId, feature, tripId). Zod validates all inputs.

response headers
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

offline.cache

sw.ts — cache config
/api/trips/[id]/*NetworkFirst
ttl: 24hmax: 100 entries

Expenses, itinerary, members, comments

/api/tripsNetworkFirst
ttl: 24hmax: 10 entries

Trip list

*.js|*.css|*.woff2StaleWhileRevalidate
ttl: 30 daysmax: 200 entries

App shell

// 3s network timeout before cache fallback. View your itinerary on a plane.

// skipWaiting + clientsClaim = instant SW activation. No reload needed.

principles

  • #Ship trunk-based. Feature flags over long-lived branches.
  • #No loading spinners for cached data. Stale-while-revalidate everywhere.
  • #Every API route has rate limiting. No exceptions.
  • #Errors are user-facing messages, not stack traces.
  • #Mobile-first. Every feature works on a phone screen.
  • #Accessibility isn't a phase. Keyboard nav + screen reader tested.
colophon

$ triplink --version

Built by one developer. Designed to feel like a team of ten built it.

Every line of code exists because a real trip needed it.

$ _

Mass over-engineered. Want to try it?

All this tech disappears behind a simple interface. Start a trip in 60 seconds.

free tier = 3 trips · all features · no credit card