A full-stack real-time auction platform for fine art, built as an open-source reference application: atomic bidding with Redis Lua scripts, four-eyes moderation, workspace-isolated multi-tenancy, wallet accounting, automated bidding agents, and hybrid REST + WebSocket communication.
The demo environment is self-seeding: role-based login buttons create isolated workspaces with auctions, wallets, notifications, randomized timers, and bot activity so the full product can be explored immediately.
![]() Landing Page |
![]() User Dashboard |
![]() Auction Detail |
![]() Support Panel |
![]() Admin Panel |
![]() Inspection Viewer |
Every bid is checked and written inside Redis with a Lua script: newBid > currentPrice and price update happen in one operation. The API holds wallet funds before the Redis call to fail fast, refunds the hold if the bid loses the race, and persists to MongoDB and broadcasts via SignalR only after Redis accepts the bid. Integration tests fire parallel bids against real Redis and assert that only the highest accepted bid wins.
Suspicious listings move through a two-person moderation path: users report, support freezes, and admins ban or restore. One report per user per auction is enforced server-side, and no single role can both flag and permanently remove content.
Every entity carries an indexed WorkspaceId, and the JWT includes a workspace_id claim. MongoDB queries filter by workspace before business logic runs, so demo sessions and user data remain isolated at the query layer.
Writes go through authenticated REST endpoints with validation, wallet logic, and RFC 7807 error responses. Live reads such as bid updates and auction endings are pushed through a public read-only SignalR hub so auction pages update without polling.
Each demo login creates an isolated workspace from template auctions, users, wallets, notifications, and automated bidding agents. The seeder assigns randomized auction windows and starts five bot bidders so live bidding, outbid alerts, auction endings, wallet holds/refunds, and SignalR updates can be tested immediately.
Bot behavior is intentionally varied:
| Profile | Purpose |
|---|---|
| Nibbler | Deterministic bidding schedule for user-created auctions |
| Ambient | Low-frequency activity for seeded inventory |
| Pacer | Delayed counter-bids after real user activity |
| Role | Dashboard | Capabilities |
|---|---|---|
| USER | Card grid with tabs | Browse, bid, create auctions, manage wallet |
| SUPPORT | Moderation queue | Review reports and freeze suspicious listings |
| ADMIN | Compliance audit panel | Ban frozen items, restore items, oversee reports |
Wallets support top-up, withdraw, bid holds, outbid refunds, and sale revenue. Withdrawals and deposits use atomic MongoDB updates with cumulative caps, so balance checks and compliance limits cannot race under concurrent requests.
Auction cards use priority-based glow states: gold for outbid, green for activity on your listing, blue for unrelated bid activity, and pink for expiring auctions. Countdown progress uses bounded transform updates instead of layout-heavy width changes.
Notifications persist in the database, update the toolbar badge, and cover auction won/lost/expired/sold, outbid alerts, seller bid alerts, and wallet operations. Unread count also updates the browser tab title.
JWTs are stored in HttpOnly, Secure, SameSite=Lax cookies, never in browser storage. The frontend never handles raw tokens. A cross-tab guard detects when another tab logs in as a different user and redirects stale sessions.
- FluentValidation on all request DTOs through a global validation filter.
- Production-only auth rate limits behind Caddy-forwarded client IP.
- HTML tag stripping before persistence.
- Password pre-hash with pepper + bcrypt.
- Typed MongoDB builders and LINQ only; no raw string queries.
Preset artwork is generated into thumb and detail WebP variants so repeated surfaces do not ship the raw original asset. The inspection viewer supports bounded zoom, pan, pinch, and loupe behavior with GSAP limited to open/close, snap-back, and tracking transitions.
Demo workspaces are hard-deleted after 24 hours by a six-hour Janitor cycle. Dashboard and moderation sort preferences are persisted in URL query parameters, with independent sort state per user dashboard tab.
┌──────────────────────────────────────────────────────────┐
│ Caddy │
│ TLS termination + reverse proxy │
│ /backend/* → API:5000 /* → UI:3000 │
└────────────────┬─────────────────────┬───────────────────┘
│ │
┌────────────▼──────────┐ ┌───────▼──────────┐
│ .NET 10 WebAPI │ │ Next.js 16 SSR │
│ Controllers + REST │ │ App Router │
│ SignalR Hub │ │ React 19 │
│ Background Services │ │ Material UI v7 │
│ FluentValidation │ │ Zustand + RQ5 │
└───┬──────────┬────────┘ └──────────────────┘
│ │
┌────▼───┐ ┌──▼────┐
│MongoDB │ │ Redis │
│ 8.0 │ │Alpine │
└────────┘ └───────┘
Bid flow:
- Frontend sends
POST /Auctions/{id}/bid. - API validates JWT cookie and workspace scope.
- Service places a wallet hold for the bid amount.
- Redis Lua performs atomic check-and-set. Lost races refund the hold.
- Service persists the bid and refunds the previous highest bidder.
- SignalR broadcasts the accepted bid to connected clients.
| Entity | Responsibility |
|---|---|
Auction |
Listing metadata, status, timing, image surface, owner, workspace |
Bid |
Accepted bid history after Redis confirms the winning price |
Wallet |
Balance plus cumulative top-up/withdrawal counters |
WalletTransaction |
Ledger entries for top-up, withdraw, hold, refund, sale revenue |
Report |
User-generated moderation signal with reporter uniqueness guard |
Notification |
Persisted product events for bid, wallet, sale, and moderation flows |
The runtime keeps fast-changing price state in Redis and durable business records in MongoDB. MongoDB remains the source of truth for history, moderation, wallet transactions, and workspace-scoped reads.
Auction items are polymorphic: paintings, sculptures, and digital pieces carry different attributes. A document model stores each listing as one complete object, maps cleanly to the TypeScript API shape, and avoids EAV tables or joins for auction detail rendering.
- Compound indexes on
WorkspaceIdacross all collections. - Redis Lua resolves bid contention in one round trip without distributed locks.
- Targeted reads fetch latest bid with
SortByDescending().Limit(1). - SignalR pushes live bid and auction ending updates.
- Shared frontend clock avoids per-card timers.
- Artwork variants ship optimized
thumbanddetailsurfaces. - Inspection viewer keeps zoom/pan in bounded transform math.
| Concern | Guardrail |
|---|---|
| Bid races | Redis Lua accepts only one winning price update |
| Wallet races | Atomic MongoDB updates enforce balance and cumulative caps |
| Demo growth | Janitor hard-deletes stale workspaces after the retention window |
| Cross-tenant reads | WorkspaceId is part of every scoped query |
| Auth brute force | Production auth rate limits use forwarded client IP |
| UI churn | Countdown/progress animations use shared clocks and transforms |
.NET 10
- Controller-based WebAPI with attribute routing.
IHostedServiceworkers for bots and cleanup.- SignalR hub for bid broadcasts and auction endings.
- RFC 7807
ProblemDetailsvia global exception handling. - Redis Lua scripts for race-free bidding.
- MongoDB typed filters and compound indexes.
Next.js 16 + React 19
- App Router with SSR for legal/SEO pages and CSR for interactive dashboards.
- Zustand for session/theme state and TanStack Query v5 for server state.
@microsoft/signalrhooks for live auction updates.- Material UI v7 theming with
sxandstyled(). - Web Audio API synthesized sounds with no external files.
Representative endpoints:
| Area | Endpoint Pattern |
|---|---|
| Auth | POST /Auth/login, POST /Auth/register, POST /Auth/demo/{role} |
| Auctions | GET /Auctions, POST /Auctions, GET /Auctions/{id} |
| Bidding | POST /Auctions/{id}/bid |
| Moderation | POST /Auctions/{id}/report, freeze, ban, restore |
| Wallet | balance, top-up, withdraw, transaction history |
| Notifications | unread count, list, mark-read, clear-all |
SignalR publishes accepted bids and auction ending events; mutation endpoints stay REST-only so validation, authorization, and wallet logic remain centralized.
auction/
├── backend/
│ ├── BackgroundServices/ # bot bidding strategies + Janitor cleanup
│ ├── Controllers/ # auth, auctions, admin, wallet, notifications
│ ├── DTOs/ # request/response contracts
│ ├── Hubs/ # SignalR live auction updates
│ ├── Middleware/ # validation + RFC 7807 exception handling
│ ├── Models/ # auctions, bids, reports, users, wallets
│ ├── Services/ # bidding, wallet, seeding, JWT, sanitization
│ ├── Tests/ # unit + integration + Testcontainers
│ └── Validators/ # FluentValidation request guards
│
├── frontend/
│ ├── e2e/ # Playwright workflows
│ ├── public/images/presets/ # artwork originals + WebP variants
│ ├── scripts/ # image variant generation
│ └── src/
│ ├── app/ # Next.js routes and dashboards
│ ├── components/ # cards, panels, wallet, viewer, notifications
│ ├── hooks/ # SignalR + notification hooks
│ └── lib/ # timing, sounds, query, session, image sources
│
├── k8s/ # K3s manifest
├── docker-compose.yml # production compose stack
├── docker-compose.local.yml # local MongoDB + Redis
└── README.md
| Component | Technology |
|---|---|
| Framework | .NET 10 WebAPI |
| Language | C# |
| Database | MongoDB 8.0 |
| Cache | Redis Alpine + Lua |
| Real-time | SignalR |
| Auth | JWT in HttpOnly cookies |
| Validation | FluentValidation |
| Testing | xUnit + NSubstitute + FluentAssertions + Testcontainers |
| Component | Technology |
|---|---|
| Framework | Next.js 16 |
| Language | TypeScript strict mode |
| UI | Material UI v7 |
| State | Zustand + TanStack Query v5 |
| Real-time | @microsoft/signalr |
| Testing | Vitest + React Testing Library + Playwright |
| Audio | Web Audio API |
| Component | Technology |
|---|---|
| Containers | Docker + Docker Compose |
| Orchestration | Kubernetes / K3s |
| Reverse Proxy | Caddy |
| CI/CD | GitHub Actions |
| Coverage | Coverlet OpenCover XML + Codecov |
- Docker and Docker Compose.
- .NET 10 SDK.
- Node.js 24+ and npm.
- WSL2 recommended on Windows.
git clone https://github.com/vladyslavm-dev/auction.git
cd auction
docker compose -f docker-compose.local.yml up -dRun the backend:
cd backend
dotnet watch run --urls "http://localhost:5000"Run the frontend:
cd frontend
npm install
npm run devOpen http://localhost:3000 and use the User, Support, or Admin demo buttons.
Docker Compose and K3s deployment paths are both represented in CI/CD. Deployment guides will be finalized after production hardening.
| Mode | Purpose |
|---|---|
docker-compose.local.yml |
Local MongoDB + Redis for development |
docker-compose.yml |
Production service stack behind Caddy |
k8s/auction-manifest.yaml |
K3s deployment path for the same service boundaries |
The application is built as separate API and UI images. Caddy terminates TLS and routes /backend/* to the API while serving the Next.js frontend for all other paths.
All endpoints return RFC 7807 ProblemDetails on error. Auth endpoints are public; mutation endpoints require a valid auction_token cookie.
Three layers of automated tests cover business logic, data integrity, and full user journeys.
(cd backend/Tests && dotnet test)
(cd frontend && npm run test)
(cd frontend && npm run build)
(cd frontend && npm run test:e2e)Representative coverage:
- Atomic Redis bidding, persistence, wallet holds/refunds, and contention handling.
- Seeder and cleanup integration flows with real MongoDB + Redis containers.
- Auth, demo access, auction creation, reporting, and moderation entry points.
- Wallet operations, bidding rules, validators, JWT, password hashing, and sanitization.
Representative coverage:
- Countdown and expiry behavior.
- Bid controls and error recovery.
- Inspection viewer zoom, pan, and responsive behavior.
- Wallet drawer and live balance refresh.
- Moderation filters and dashboard default state.
auction-flow.spec.ts validates auth, bidding, moderation, wallet operations, notifications, image inspection, and responsive behavior.
Two GitHub Actions workflows follow the same pattern: Test → Build → Push → Deploy.
| Workflow | Trigger | Target |
|---|---|---|
deploy-docker.yml |
Push to main |
EC2 via Docker Compose |
deploy-k8s.yml |
Manual dispatch | EC2 via K3s + kubectl apply |
Both workflows run backend and frontend tests, build multi-stage Docker images, push to Docker Hub, and deploy through SSH.
Auction item images are provided by the National Gallery of Art under their Open Access (CC0) policy.
MIT License. Copyright (c) 2026 Vladyslav Marchenko
See LICENSE for details.
Vladyslav Marchenko
- GitHub: @vladyslavm-dev
- Website: vladyslavm.dev





