Skip to content

GavL – Next Auctions

Built Sep–Nov 2025

Bidding · Stripe Payments (Test Mode) · Postgres Triggers · Realtime Notifications

SBSiegfried Bozza
GavL dashboard: auction, bid and user counts above the auctions table
GavL landing page with the Browse Auctions and Create Auction buttons

Real-time bid flow

DB triggers and per-user Supabase Realtime notifications (Next Auctions README).

Overview

Auction platforms need bidding, automatic closure at the deadline, notifications to the people involved, and payment settlement without exposing privileged database access to the browser.

GavL keeps the workflow in Postgres: SECURITY DEFINER triggers track the highest bid and write NEW_BID, NEW_AUCTION_WON and NEW_PAYMENT notifications, pushed to each user over a private Supabase Realtime channel. A pg_cron job calls the close-auctions Edge Function every 5 minutes to close due auctions, and a signature-verified Stripe webhook records payment through a server-only service role.

Technical Architecture

GavL is a learning build focused on real-time full-stack flows. It follows a ports-and-adapters layout: domains hold types and Zod schemas; ports are repository interfaces; services own workflows; Supabase repositories sit behind those ports; instances wire services to infrastructure. Notifications for new bids, auction wins and payments go to each affected user over private per-user Supabase Realtime broadcast channels; other viewers see a new price on refresh. A pg_cron job calls the close-auctions Edge Function every 5 minutes, so auctions close up to 5 minutes after endAt; the repo commits it as a migration that reads its URL and key from Vault, and the live project runs the same job, created in the dashboard. Stripe Checkout plus a signature-verified webhook (raw body, constructEvent) sets paidAt through a service-role client that stays server-only. The UI is multilingual (French, English, German via Lingui), responsive, and uses URL-driven filters, sorting, and pagination (nuqs) with Suspense and skeleton loaders.

Key features

  • Bid Tracking in Postgres: Triggers keep the highest bid and bidder on the auction row; a UNIQUE (auction, amount) index rejects tied bids; RLS stops owners from editing or deleting an auction once it has bids.
  • End-to-End Auction Lifecycle: Creation, editing, bidding, scheduled closure (Edge Function), winner notifications, and payment completion.
  • Stripe Payments: Stripe Checkout with a signature-verified webhook for server-side payment status; /api is excluded from the i18n middleware so Stripe is never redirected.
  • Notification System: Real-time notifications for new bids, auction wins and payments, pushed to each affected user over a private Supabase Realtime channel; the payment notification fires only on the first paid_at write.
  • Server-Side Table Filtering: URL-driven filters, sorting, and pagination using nuqs for consistent server-client synchronization.
  • Multilingual UI: Localized interface in French, English, and German via Lingui with locale-aware routes.
  • Ports-and-Adapters Layering: Services behind repository interfaces, with Jest unit tests on services and mappers run in CI (lint, build, coverage); Supabase declarative schemas with diff-generated migrations.

Technologies Used

Full-stack auction case study: Postgres triggers that track the highest bid and send per-user Realtime notifications, RLS, a scheduled Edge Function that closes auctions, and Stripe in test mode (Next.js, TypeScript, Supabase). Known bid-integrity gaps, listed under Known limitations.
  • Next.js
  • Type­Script
  • Supabase
  • Stripe
  • React
  • Tan­Stack Table
  • React Hook Form
  • Zod
  • nuqs
  • Lingui
  • Recharts
  • shadcn/ui
  • Tailwind CSS
  • Jest
  • Frontend: Next.js, TypeScript, React
  • Backend & Database: Supabase (Postgres triggers, RLS, Realtime, Edge Functions, pg_cron)
  • Payments: Stripe Checkout & Webhooks
  • UI & Form Handling: shadcn/ui, React Hook Form, Zod
  • Filtering & Data Display: nuqs, TanStack Table, Recharts (payments dashboards)
  • Internationalization: Lingui: French / English / German with locale-aware routes.
  • File Uploads: Uppy
  • Testing: Jest unit tests, CI (lint, build, coverage)
  • Architecture: Ports and adapters: domains, repository interfaces, services and wired instances

Challenges and Learnings

Designing a bidding flow that stayed understandable while triggers, the scheduled Edge Function and Stripe webhooks all touched related rows. RLS had to protect normal clients while the webhook wrote through a service-role client whose key never reaches the browser. Keeping notifications, auction state and payment flags aligned: the payment trigger fires only when paid_at goes from NULL to set, so a webhook retry does not notify twice.

Outcome

GavL demonstrates an end-to-end auction flow at next-auctions.vercel.app: bidding, scheduled closure, Stripe payments in test mode, real-time notifications, multilingual UI (French, English, German), dashboards with charts, and a ports-and-adapters codebase with Jest unit tests. It complements the Web3 case studies with full-stack work on payments, Postgres triggers and RLS, notifications, i18n and testing.

Key Metrics

  • Live demo: next-auctions.vercel.app
  • i18n: French / English / German (Lingui)
  • Architecture: ports and adapters, services behind repository interfaces; URL-driven filters via nuqs; Jest unit tests in CI
  • Automation: pg_cron calls the close-auctions Edge Function every 5 minutes; signature-verified Stripe webhook → server-only service-role write

Transferable patterns: Payments + webhooks, workflow rules in Postgres triggers and RLS, per-user notification channels, and scheduled lifecycle jobs.

Known limitations

  • Known bugs, bid integrity: the server does not check that a bid beats the current highest (only the form does); RLS lets bidders edit or delete their own bids while an auction is open; owners can bid on their own auctions; bids are accepted between endAt and the next scheduled close, up to 5 minutes later; and the highest-bid trigger reads the top bid without a lock, so two near-simultaneous bids can store a stale highest bid. The fix is a place_bid RPC that locks the auction row and checks amount and deadline, with no update or delete on bids.
  • Payment checks are UI-only: creating a checkout session does not check on the server that the caller is the winner or that the auction is closed and unpaid; the webhook then trusts the auction ID in the session metadata.
  • Webhook retries are not deduplicated by event.id; a retry rewrites paidAt, but the payment notification still fires once.
  • Testing depth: 66 Jest tests cover services and mappers with mocked repositories; triggers, RLS, the webhook, the Edge Function and the server actions are untested, and there is no end-to-end browser harness yet.
  • Service role: the Stripe webhook and the Edge Function bypass RLS with a server-only key. One server action that writes through it has no auth check of its own; today only the webhook route imports it.
Next projectLabsTrailTraceable Paper Ratings on the OriginTrail DKG and Base Sepolia