Shipping·9 min read

From idea to deployed in a weekend

Most "build in a weekend" advice fails because it optimizes for the demo, not the deploy. This is the opposite: a playbook for actually shipping something real people can use by Sunday night — scoped tight, wired to boring proven tools, and live on a URL. The craft is in what you refuse to build.

Cut the idea in half, then cut it again

The single biggest predictor of whether you ship this weekend is how brutally you scope. Your idea has one core action — the thing that makes it worth existing. For a habit tracker it's 'log a habit'; for a link-in-bio tool it's 'edit a page and view it public'. Everything else — teams, settings pages, dark mode toggles, onboarding tours — is a distraction that feels like progress. Write down the one sentence a user would say to describe what your app did for them, and delete every feature that isn't load-bearing for that sentence. You are not building a product this weekend; you're building the smallest thing that proves the core loop works when it's live and someone else is holding it.

Pick boring tools you don't have to think about

A weekend is won or lost on integration friction, so use the stack where the pieces already know about each other. In 2026 the highest-leverage default for a vibe coder is Next.js on Vercel, with Supabase or Neon for Postgres, Drizzle ORM for typed queries, Clerk for auth, Stripe for payments, and Resend for email. These aren't the 'best' tools in some abstract sense — they're the ones with the fewest sharp edges, the best docs an AI has actually seen thousands of times, and one-click deploy paths. Do not evaluate alternatives this weekend. Every hour spent comparing Auth.js vs Clerk vs Lucia is an hour not shipping. Novelty in your stack is risk; put your novelty in the product.

text
The weekend default stack:
  Framework   Next.js (App Router)
  Host        Vercel
  Database    Supabase / Neon (Postgres)
  ORM         Drizzle
  Auth        Clerk
  Payments    Stripe
  Email       Resend
Don't deviate unless you've shipped this exact combo before.

Deploy on Saturday morning, not Sunday night

The classic mistake is building the whole thing locally and treating deployment as the last step. Invert it. The first thing you should ship is a nearly-empty app — one page that says the name of your product — pushed to a Git repo and connected to Vercel so it's live on a real URL before you've written any real logic. This does two things: it front-loads all the environment-variable and build-config pain while the app is trivial enough to debug in minutes, and it means every subsequent commit is continuously deployed. When you hit the classic 'works on my machine' gap — a missing env var, a Node version mismatch, an edge-runtime import that dies in production — you find it Saturday morning against ten lines of code, not Sunday at 11pm against your whole app.

Let the database schema anchor the whole build

The schema is the one decision that's expensive to change once you have real users, so it's worth thinking about for twenty focused minutes even in a weekend build. Model your core entities, put real foreign keys and NOT NULL constraints in from the start, and lean on the database to enforce correctness so your app code can stay dumb. With Drizzle you define the schema in TypeScript and get typed queries for free, which means the AI writing your queries can't hallucinate a column that doesn't exist. Add a unique constraint anywhere two rows shouldn't collide (one vote per user per post, one slug per account) — it's a two-word change now and a data-cleanup nightmare later. Resist adding speculative columns for features you cut in section one.

typescript
// schema.ts — constraints are cheap now, priceless later
export const votes = pgTable('votes', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: text('user_id').notNull(),
  postId: uuid('post_id').notNull().references(() => posts.id),
  createdAt: timestamp('created_at').defaultNow().notNull(),
}, (t) => ({
  // one vote per user per post — enforced by Postgres, not hope
  uniq: unique().on(t.userId, t.postId),
}));

Drive the AI with the loop, not the feature list

When you're pairing with an AI to build fast, the failure mode is asking for a whole feature ('build me a dashboard with charts and filters and export') and getting 400 lines you can't verify. Instead, walk the user's actual loop one step at a time: sign up, create the thing, see the thing, edit the thing, share the thing. Build and manually test each step before starting the next. This keeps every diff small enough that you can actually read it, and it surfaces integration bugs at the seam where they happen rather than three features later. Give the AI the real error text and the real file, not a paraphrase — 'here's the stack trace and the route handler' beats 'the save button doesn't work.' And when something works, commit immediately so you always have a known-good point to roll back to.

Handle the unglamorous 20% that makes it real

The gap between a demo and a deployed app is almost entirely error states and empty states — the parts that never show up in a happy-path screen recording. Every form needs a loading state and a visible failure message, or users will double-submit and blame you. Every list needs an empty state that tells a first-time user what to do, because they will all see it before they see a full one. Every mutation that touches money or sends email needs to be idempotent or guarded, because Stripe and Resend webhooks retry. This is where you spend Sunday afternoon, and it's the difference between 'I made a thing' and 'someone else used my thing without messaging me confused.' Skip auth-adjacent security theater but never skip these three: input you don't trust, network calls that can fail, and operations you can't afford to run twice.

Ship the secrets and the boundaries correctly

The two ways a weekend project embarrasses you publicly are leaked keys and unprotected mutations. Keep every secret in environment variables set in the Vercel dashboard, never in the repo — and know that in Next.js any variable prefixed NEXT_PUBLIC_ is shipped to the browser, so your Stripe secret key and database URL must never carry that prefix. On the server, actually check authorization on every mutation: 'is the logged-in user allowed to edit this specific row,' not just 'is someone logged in.' It's the single most common real vulnerability in AI-built apps, because the AI happily writes an update query keyed only on the row ID it was handed. One ownership check per mutation closes it.

typescript
// Every mutation: authenticate AND authorize
const { userId } = await auth();               // Clerk: are you logged in?
if (!userId) throw new Error('unauthenticated');

const post = await db.query.posts.findFirst({ where: eq(posts.id, id) });
if (post?.authorId !== userId) throw new Error('forbidden'); // is it YOURS?

await db.update(posts).set({ title }).where(eq(posts.id, id));

Ship it ugly, then stop

Done and deployed beats polished and local, every single time. Your Sunday-night version should have a plain, consistent design (pick one component library — shadcn/ui is the safe default — and don't fight it), the core loop working end to end, and a real URL you can send to someone. Then genuinely stop. The instinct to add 'just one more feature' before showing anyone is the instinct that turns weekend projects into graveyards. Put it in front of five real people Monday and let their confusion — not your imagination — write the next backlog. The whole point of shipping in a weekend is to buy real feedback cheaply; you only collect on that if you actually let go of it while it's still small.

Takeaways

  • Scope to a single core loop and delete everything that isn't load-bearing for it
  • Deploy an empty app to a real URL Saturday morning so env/build pain is cheap to debug
  • Use the boring, well-integrated default stack — put your novelty in the product, not the tooling
  • Build the user's loop one verifiable step at a time and commit every time it works
  • Error states, empty states, and idempotency are the 20% that makes a demo into a real app
  • Authorize every mutation by ownership, and never prefix a secret with NEXT_PUBLIC_