$ cat system.md
For the curious. The full technical colophon.
dependencies
framework
- next15.xApp Router, RSC, streaming
- react19.xServer components default
- typescriptstrictZero any policy
data
- postgresqlNeonServerless, branching
- prismaORM55 models, typed queries
- upstash/redisRESTRate limits, cache
auth
- clerk5.xSSO, orgs, RBAC
payments
- stripeSDKCheckout, webhooks, subscriptions, metered billing
ai
- openaiGPT-4oEmail parsing, trip assistant, NL commands
messaging
- twilioSDKSMS + WhatsApp
- resendAPITransactional email
- @slack/sdklatestBot + slash commands
observability
- sentrySDKError tracking, tracing
- vercel/insights—Web Vitals, speed
- ga4—Funnels, retention
infra
- vercelEdge + NodeZero-config deploys
- serwist9.xPWA, offline-first
ui
- tailwindcssv4Utility-first
- lucideiconsTree-shakeable
- cmdk—Command palette (Cmd+K)
- dnd-kit—Drag & drop itinerary
- sonner—Toast notifications
maps
- leafletOSMInteractive maps, no Google tax
testing
- vitestunitFast, ESM-native
- playwrighte2eCross-browser
- storybook8.xComponent isolation
architecture
render: 'server'Server components by default. Client boundaries only where you click, type, or drag.
cache: TanStack QueryClient-side cache with optimistic updates. Mutations feel instant.
rateLimit: UpstashRedis-backed rate limiting on all API routes. Sliding window algorithm.
offline: SerwistService worker pre-caches the app shell. View your itinerary on a plane.
partner: white-labelB2B partner system. Custom branding, subdomain routing, org-scoped data.
notify: multi-channelSame event fires to email, SMS, WhatsApp, Slack, and in-app. User picks.
features --verbose
"Split the Airbnb 4 ways" → creates expense, assigns members, calculates shares
Forward a booking confirmation → AI extracts dates, location, confirmation # → adds to itinerary
Scan to join. No app download. Deep links to the right trip.
Subscribe URL auto-updates when the itinerary changes
Print-friendly. Includes maps, times, addresses, notes.
Real-time delay/gate change alerts pushed to the group
Optimistic UI. Multiple people editing the same trip, no conflicts.
pricing --explain
| tier | trips | members | credits | itinerary | expenses | price |
|---|---|---|---|---|---|---|
| FREE | 3 | 10 | 10/mo | 5/day | 20/trip | $0 |
| PRO | ∞ | 50 | 100/mo | ∞ | ∞ | $4.99/mo |
| TEAM | ∞ | ∞ | 500/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
Each AI message costs exactly 1 credit
Fast, cheap, good enough for trip ops
Per member. Sliding window.
Per message. Keeps responses focused.
Max simultaneous conversations per member
Members use the trip owner's credit budget on TEAM plans
Input/output tokens tracked. Total cost stored per conversation.
Rate limits configurable via AIConfig table. No redeploy needed.
expenses.split()
EQUALCent-based integer arithmetic. Penny remainder distributed round-robin.
PERCENTAGEExtra pennies go to members with largest fractional parts.
ITEMIZEDPer-item equal splits, aggregated across all items.
EXACTSpecify 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()
Notification model with type enum (8 types), linkPath for deep linking, metadata JSON
Rich block messages with action buttons. Per-channel toggles: expenses, polls, itinerary, members
Plain text with markdown. Daily summary digest at configurable time. Trip countdown.
Resend API. Booking confirmation parser on inbound webhook.
Twilio. Used for critical alerts (flight delays, gate changes).
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.
offline.cache
/api/trips/[id]/*NetworkFirstExpenses, itinerary, members, comments
/api/tripsNetworkFirstTrip list
*.js|*.css|*.woff2StaleWhileRevalidateApp 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.
$ 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