# I Built a Full-Stack B2B Marketplace in 3 Days With V0, Next.js 16, and Vercel's AWS Integration — Here's What Actually Happened

> Source: <https://dev.to/arek_h/i-built-a-full-stack-b2b-marketplace-in-3-days-with-v0-nextjs-16-and-vercels-aws-integration--4ma4>
> Published: 2026-06-27 08:20:08+00:00

*The honest account: what worked, what broke, and what I'd do differently.*

Three days. One B2B Dutch-auction marketplace. Here's the realistic version of what building with V0, Next.js 16 App Router, Aurora PostgreSQL, and DynamoDB actually looks like when you're going fast.

This is not a "look how smooth AI coding is" post. It's the honest version.

Let me start with the honest assessment. V0 is genuinely impressive at one specific thing: generating multi-page UI from a detailed prompt and producing something that's 70–80% of the way to production-quality on the first pass.

Here's the prompt structure that worked:

```
Build [app name]. DO NOT provision any databases.

WHAT IT DOES: [one paragraph, clear]
BUILD THESE PAGES: [numbered list, each with specific UI requirements]
DESIGN DIRECTION: [specific hex colors, font choices, component library]
DO NOT: [explicit list of what not to do]
```

The "DO NOT" section is not optional. V0 will try to add authentication, spin up a database, create server actions, and add 47 npm packages you didn't ask for if you don't explicitly forbid it. Write the constraint list first.

The result after one well-structured prompt:

Genuinely good output. The thing that V0 gets right is visual design decisions — the layout hierarchy, the card structure, the urgency indicators. These are the things that take hours to hand-craft and minutes to generate.

**What V0 doesn't handle well:** data wiring. V0's proxy routes work for simple cases but any non-trivial API shape requires going back and being very explicit. Prompt 2 (wiring to real data) required more back-and-forth than Prompt 1 (building the UI).

**The annotation feature is underused.** In V0 Max, you can click any element in the preview and leave an annotation. "Make this number bigger." "This progress bar should be red when below 20%." This is dramatically faster than re-prompting the whole component. Use it.

This one cost an hour.

In Next.js 15+, route params are now a Promise. If you have a route like `app/api/drops/[dropId]/route.ts`

, the params signature changed:

```
// Next.js 14 (old)
export async function GET(
  req: NextRequest,
  { params }: { params: { dropId: string } }
) {
  const { dropId } = params; // fine
}

// Next.js 16 (current)
export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ dropId: string }> }
) {
  const { dropId } = await params; // required
}
```

The error message when you get this wrong is:

```
Type 'typeof import("...route")' does not satisfy the constraint 
'RouteHandlerConfig<"/api/drops/[dropId]">'.
```

This error is not particularly clear about what's actually wrong. The fix is `await params`

everywhere you destructure route segment params. I had three routes that needed fixing.

The Vercel AWS Marketplace integration is genuinely slick. You click through a wizard in the Vercel dashboard and it:

The result: your serverless functions authenticate to AWS databases using short-lived OIDC tokens with zero stored credentials. No Secrets Manager, no password rotation, no long-lived access keys.

The code is clean:

``` js
import { awsCredentialsProvider } from "@vercel/functions/oidc";
import { Signer } from "@aws-sdk/rds-signer";

const signer = new Signer({
  hostname: process.env.PGHOST!,
  port: Number(process.env.PGPORT ?? 5432),
  username: process.env.PGUSER!,
  region: process.env.AWS_REGION!,
  credentials: awsCredentialsProvider({
    roleArn: process.env.AWS_ROLE_ARN!,
    clientConfig: { region: process.env.AWS_REGION },
  }),
});

const pool = new Pool({
  host: process.env.PGHOST,
  password: () => signer.getAuthToken(), // fresh token per connection
  ssl: { rejectUnauthorized: false },
});
```

**The caveats:**

**1. Environment variable prefix chaos.** When you install two AWS integrations (we installed both Aurora and DynamoDB), the env var prefixes differ. Aurora uses no prefix (`PGHOST`

, `AWS_ROLE_ARN`

). DynamoDB, when installed second, gets a prefix that depends on what you name it during setup — ours ended up as `DYB_`

(`DYB_AWS_ROLE_ARN`

, `DYB_DYNAMODB_TABLE_NAME`

). This isn't documented prominently. Check your actual `.env.local`

after running `vercel env pull`

.

**2. Development environment checkbox.** When connecting integrations to your project, there's a checkbox for which environments (Production, Preview, Development) get the env vars. It's easy to miss Development. If `vercel env pull`

returns only `VERCEL_OIDC_TOKEN`

, this is why — go back to the integration settings and check Development.

**3. Local OIDC auth depends on the IAM role trust policy.** The `VERCEL_OIDC_TOKEN`

that `vercel env pull`

injects locally is a development token from a different issuer than the production token. Whether it works locally depends on whether the IAM role's trust policy includes the development issuer. Ours didn't initially, so `npm run db:test`

failed locally even though production worked fine. The pragmatic fix: add `dotenv`

to `aurora.ts`

so the module works in both environments, and test primarily against the deployed URL.

**4. The dotenv import must precede all AWS imports.** In ES modules, static imports are hoisted and executed before runtime code — including

`config()`

. This means:

``` js
// WRONG: AWS SDK is imported before .env.local loads
import { awsCredentialsProvider } from "@vercel/functions/oidc";
import { config } from "dotenv";
config({ path: ".env.local" });
js
// CORRECT: dotenv first
import { config } from "dotenv";
config({ path: ".env.local" });
import { awsCredentialsProvider } from "@vercel/functions/oidc";
```

This caused a `Value null at 'roleArn'`

error that took longer than it should have to diagnose.

With a 3-day deadline, there was no time to set up Prisma or Drizzle. The approach: a single `/api/migrate`

route protected by a `MIGRATE_SECRET`

env var. Hit it once after deploy and all tables are created:

```
export async function POST(req: NextRequest) {
  const { secret } = await req.json();
  if (secret !== process.env.MIGRATE_SECRET) {
    return NextResponse.json({ error: "unauthorized" }, { status: 401 });
  }
  await query(`CREATE TABLE IF NOT EXISTS brands (...)`);
  await query(`CREATE TABLE IF NOT EXISTS lots (...)`);
  // etc.
  return NextResponse.json({ ok: true });
}
```

This is not a production pattern. It's a hackathon pattern that works fine when your schema is stable and you control when migrations run. For a demo with a 3-day deadline, it's the right call.

Dutch-auction drops expire. A 3-hour drop is great for live demo urgency but terrible for a submitted hackathon project that judges might visit at 11pm three days later.

The fix: a `/api/ensure-drops`

endpoint that checks how many live, unexpired drops exist and reseeds automatically if the count falls below 3:

``` js
export async function GET() {
  const countResult = await query<{ count: string }>(
    `SELECT COUNT(*) AS count FROM drops
     WHERE status = 'live' AND ends_at > NOW()`
  );
  const liveCount = parseInt(countResult[0].count, 10);
  if (liveCount >= 3) {
    return NextResponse.json({ seeded: false, liveDrops: liveCount });
  }
  await seedAllDrops(); // creates 6 new drops with fresh timers
  return NextResponse.json({ seeded: true, liveDrops: 6 });
}
```

The frontend calls this silently on every page load via `useEffect`

. A Vercel cron job calls it daily as a backup. Judges always see a live marketplace regardless of when they visit.

The cron configuration in `vercel.json`

(note: Hobby plan limits to daily frequency):

```
{
  "crons": [
    {
      "path": "/api/ensure-drops",
      "schedule": "0 0 * * *"
    }
  ]
}
```

After 3 days:

The thing I'm most satisfied with is the claim handler. It does exactly one thing — decrement inventory atomically, record the transaction relationally — and it does it correctly under concurrent load. That's the core of the product.

The thing I'd do differently: start the database connectivity earlier. The OIDC auth quirks and env var prefix confusion cost nearly a day. If I'd run `vercel env pull`

and confirmed the variables immediately after installation, the rest of the build would have been smoother.

**Use V0 Max for complex multi-page builds.** The quality difference from Mini is significant. For simple components, Mini is fine.

**Check vercel env pull output immediately after installing any integration.** Note the exact variable names — they may not be what you expect.

**Put dotenv at the top of any module that initialises AWS clients.** Don't rely on the calling code to load it first.

**Test your API routes against the deployed Vercel URL, not locally, when using OIDC auth.** Local OIDC works if the role trust policy includes the dev issuer; production OIDC always works.

** await params in all App Router route handlers.** It's a breaking change in Next.js 15+. Grep for

`{ params }:`

in your route files and add `await`

before you deploy.*ClearLot was built for the H0: Hack the Zero Stack hackathon. Stack: Next.js 16, TypeScript, Tailwind, shadcn/ui, V0, Aurora PostgreSQL, DynamoDB, Vercel. Live at clearlot.vercel.app.*
