<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: rokcso</title>
    <description>The latest articles on Forem by rokcso (@rokcso).</description>
    <link>https://forem.com/rokcso</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3844396%2F6d387839-fdda-4867-8b28-6d9ff75af7ac.png</url>
      <title>Forem: rokcso</title>
      <link>https://forem.com/rokcso</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rokcso"/>
    <language>en</language>
    <item>
      <title>Building Shipstry: 640 Commits, 9 Days, One Launch</title>
      <dc:creator>rokcso</dc:creator>
      <pubDate>Thu, 26 Mar 2026 09:13:20 +0000</pubDate>
      <link>https://forem.com/rokcso/building-shipstry-640-commits-9-days-one-launch-391i</link>
      <guid>https://forem.com/rokcso/building-shipstry-640-commits-9-days-one-launch-391i</guid>
      <description>&lt;p&gt;On March 3, 2026, I started with an empty folder. On March 11, 2026, Shipstry went live.&lt;/p&gt;

&lt;p&gt;In between: 640 commits, countless cups of coffee, and a lot of lessons learned about building on the edge.&lt;/p&gt;

&lt;p&gt;This is the story of how I built it, the technical decisions I made, and what I learned along the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdvumu8k68rlnvz7atv3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdvumu8k68rlnvz7atv3.webp" alt="Shipstry.com" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Name
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of code, I needed a name.&lt;/p&gt;

&lt;p&gt;I spent an entire afternoon brainstorming with AI. I must have asked for hundreds of suggestions. The AI probably hated me by the end of it.&lt;/p&gt;

&lt;p&gt;I wanted something that captured the essence of what makers do — we &lt;strong&gt;ship&lt;/strong&gt; products. And I wanted it to feel like a registry, a place where products are officially recorded and discovered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship&lt;/strong&gt; + &lt;strong&gt;Registry&lt;/strong&gt; = &lt;strong&gt;Shipstry&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It sounded nautical, it felt right, and the .com was available. Done.&lt;/p&gt;

&lt;p&gt;The nautical theme evolved into something more organic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary color: Olive Moss (#6B8A67)&lt;/li&gt;
&lt;li&gt;Accent: Warm Sand (#D4A574)&lt;/li&gt;
&lt;li&gt;Pricing tiers: Harbor, Voyage, Expedition, Admiral&lt;/li&gt;
&lt;li&gt;The logo: a geometric sailboat with twin sails&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Why
&lt;/h2&gt;

&lt;p&gt;After launching several side projects over the years, I kept running into the same problem: &lt;strong&gt;Product Hunt is great, but it's not built for indie makers anymore.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Big-budget launches dominate. Marketing teams game the algorithm. Great products from solo developers get buried in hours.&lt;/p&gt;

&lt;p&gt;I wanted something different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A place that celebrates builders, not marketers&lt;/li&gt;
&lt;li&gt;Weekly cycles instead of daily chaos&lt;/li&gt;
&lt;li&gt;Quality over quantity&lt;/li&gt;
&lt;li&gt;Built by a maker, for makers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built Shipstry — "The Launch Registry."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack Decision
&lt;/h2&gt;

&lt;p&gt;Before writing code, I spent time on stack selection. This is the most important decision you make at the start of a project — it will haunt you for months if you get it wrong.&lt;/p&gt;

&lt;p&gt;I chose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TanStack Start&lt;/strong&gt; for the framework. It's a full-stack React framework with file-based routing and excellent TypeScript support. The type safety is incredible — if you change a route, the compiler tells you everywhere that needs updating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Workers&lt;/strong&gt; for deployment. Edge computing means my users in Singapore, London, and New York all get the same fast experience. No cold starts, global distribution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare D1&lt;/strong&gt; for the database. It's SQLite at the edge. Yes, SQLite — the same database that powers your phone, now running in 300+ locations worldwide. For a product like Shipstry, it's perfect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare R2&lt;/strong&gt; for file storage. When users upload product logos and preview images, they go here. It's S3-compatible but with zero egress fees, which means I don't have to worry about surprise bandwidth bills.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better Auth&lt;/strong&gt; for authentication. Email/password plus Google OAuth, and it integrates natively with TanStack Start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stripe&lt;/strong&gt; for payments, &lt;strong&gt;Resend&lt;/strong&gt; for emails, &lt;strong&gt;Tailwind CSS v4&lt;/strong&gt; for styling, &lt;strong&gt;shadcn/ui&lt;/strong&gt; for components.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;TanStack Start + Cloudflare&lt;/strong&gt; is a powerful combination. You get React's ecosystem with edge performance, and D1 gives you a real database with zero configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Week
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day 1-2: Foundation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first commits set up the entire foundation — TanStack Start with SSR, Cloudflare Workers adapter, Drizzle ORM, basic routing structure.&lt;/p&gt;

&lt;p&gt;I also built the design system. I didn't want another generic AI landing page with purple gradients. I created a custom "Olive Moss" palette — muted greens and warm grays that feel organic and calm.&lt;/p&gt;

&lt;p&gt;By end of Day 2, I had a working dev server, a distinctive visual identity, and basic page layouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 3-4: Authentication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Authentication is always more complicated than you expect.&lt;/p&gt;

&lt;p&gt;Better Auth needs to create its auth instance per-request, not as a singleton. In Cloudflare Workers, each request is isolated anyway, so this architecture actually works well. But figuring that out took a few hours of head-scratching.&lt;/p&gt;

&lt;p&gt;I also designed the database schema upfront. The key decision: separating &lt;strong&gt;drafts&lt;/strong&gt; from &lt;strong&gt;products&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Drafts have all nullable fields — users can save at any point in the submission flow and return later. Products have required fields — they only exist when fully submitted. This kept the data model clean and the code simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 4-5: The Submission Flow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The submission form is the heart of Shipstry. I wanted it to feel smooth, not overwhelming.&lt;/p&gt;

&lt;p&gt;I built a progressive form with collapsible sections. Each section tracks its completion status. Users can save at any point, leave, and pick up where they left off days later.&lt;/p&gt;

&lt;p&gt;For the product description, I integrated Milkdown — a plugin-driven Markdown editor with a custom toolbar. The tricky part was focus management: the toolbar kept stealing focus from the editor. I eventually fixed it by preventing default on mousedown for toolbar buttons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 5: Pricing and Payments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I designed a nautical-themed pricing system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Harbor&lt;/strong&gt; (Free): Basic submission, normal review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voyage&lt;/strong&gt; ($9.9): Fast 24-hour review, same-week ship&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expedition&lt;/strong&gt; ($29): Featured on homepage, 7 days exposure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admiral&lt;/strong&gt; ($59): 30 days featured, premium badge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stripe integration was straightforward, but the webhook handler needed careful attention. D1 doesn't support nested transactions, so I had to restructure the code to use sequential queries instead of wrapping everything in a transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Feature
&lt;/h2&gt;

&lt;p&gt;Filling out product forms is tedious. Users paste a URL and then have to manually enter the name, tagline, description, logo, preview image...&lt;/p&gt;

&lt;p&gt;So I built an AI-powered metadata fetcher.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flg8ua8xkhrthm6vjx4tk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flg8ua8xkhrthm6vjx4tk.webp" alt="✨ AI Stow..." width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a user pastes their product URL, the system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetches the page and extracts Open Graph tags&lt;/li&gt;
&lt;li&gt;Sends the information to AI to generate an enhanced, compelling description&lt;/li&gt;
&lt;li&gt;Auto-fills all the form fields&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user can review and edit everything before submitting. It's not about replacing human input — it's about reducing friction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Provider Failover
&lt;/h3&gt;

&lt;p&gt;AI APIs are unreliable. They timeout, they rate limit, they have outages.&lt;/p&gt;

&lt;p&gt;I built a failover system that tries multiple AI providers in priority order. If one fails, it automatically tries the next. The configuration is a simple JSON array in environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gemini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all providers fail, the form still works — users just fill it manually. Graceful degradation is key.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSRF Protection
&lt;/h3&gt;

&lt;p&gt;Allowing users to fetch arbitrary URLs is dangerous. You don't want someone hitting your internal services through your server.&lt;/p&gt;

&lt;p&gt;I implemented multiple layers of protection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Block private IP ranges (10.x, 172.x, 192.168.x)&lt;/li&gt;
&lt;li&gt;Block localhost&lt;/li&gt;
&lt;li&gt;Only allow HTTP and HTTPS protocols&lt;/li&gt;
&lt;li&gt;Rate limit: 5 requests per minute per user&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Community Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Comments and Voting
&lt;/h3&gt;

&lt;p&gt;Comments support nesting — users can reply to replies. I used soft deletes instead of hard deletes, so if a parent comment is removed, the threading structure stays intact.&lt;/p&gt;

&lt;p&gt;For voting, I wanted instant feedback. Nobody wants to wait for a server round-trip to see their vote register.&lt;/p&gt;

&lt;p&gt;I implemented optimistic updates: when you click vote, the UI updates immediately. The server request happens in the background. If it fails, the UI rolls back. This makes the app feel snappy and responsive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notifications
&lt;/h3&gt;

&lt;p&gt;Users get notified for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comments on their products&lt;/li&gt;
&lt;li&gt;Replies to their comments&lt;/li&gt;
&lt;li&gt;Award wins (weekly and monthly)&lt;/li&gt;
&lt;li&gt;Product status changes (approved, rejected)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For email delivery, I used Cloudflare's &lt;code&gt;waitUntil()&lt;/code&gt; function. This sends the response to the user immediately while the email sends in the background. The user doesn't wait for the email to send.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Days
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Caching&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To reduce database load, I built a caching layer using D1 itself as the cache store. Cached data has TTLs, and mutations trigger automatic cache invalidation.&lt;/p&gt;

&lt;p&gt;This pattern dramatically reduced read load on the main tables during high-traffic periods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I centralized all environment variables with validation. In development, the app validates that all required secrets exist and throws clear errors if something is missing. In production, I trust that Cloudflare has the secrets configured.&lt;/p&gt;

&lt;p&gt;This caught several configuration mistakes during development that would have been painful to debug in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On March 11, 2026, Shipstry went live.&lt;/p&gt;

&lt;p&gt;The final commits added a launch promo banner with a 50% discount code, and adjusted the ship week logic to allow immediate launches during the launch period.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TanStack Start is ready for production.&lt;/strong&gt; The framework is stable, well-typed, and SSR works seamlessly with Cloudflare Workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;D1 is good enough.&lt;/strong&gt; SQLite at the edge sounds limiting, but for most applications, it's perfect. Zero configuration, fast queries, generous free tier. The main gotcha is no nested transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge functions change how you think.&lt;/strong&gt; No global state, &lt;code&gt;waitUntil()&lt;/code&gt; for background tasks, zero cold starts, environment access through imports rather than &lt;code&gt;process.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI integration is easier than expected.&lt;/strong&gt; With the right abstraction — multi-provider failover and graceful degradation — you can build reliable AI features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;640 commits in 9 days.&lt;/strong&gt; That's roughly 71 commits per day. Each commit was small, focused, and reversible. The discipline of atomic commits saved me multiple times when I needed to roll back a bad decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened After Launch
&lt;/h2&gt;

&lt;p&gt;Shipstry has been live for two days.&lt;/p&gt;

&lt;p&gt;In that time, I've been doing link building — submitting to directories, reaching out to communities, getting featured on various platforms.&lt;/p&gt;

&lt;p&gt;The results? &lt;strong&gt;DR went from 0 to 14 in two days.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvan9j10oiosx31ki0jke.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvan9j10oiosx31ki0jke.webp" alt="Shipstry on X" width="800" height="1091"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with TanStack Start, Cloudflare Workers, D1, R2, and too much coffee.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
