The Live-Built Showcase.
This page is an evolving experiment in digital architecture. It serves as a live-built showcase of my skills where every iteration is documented, from initial wireframes to advanced features.
Quick Stats
Engineered With
Frontend
- Astro
- Tailwind CSS
- Supabase Auth
- Cloudflare Pages
Backend
- C# ASP.NET Core
- PostgreSQL
- Marten Event Sourcing
Infrastructure
- Oracle Cloud VPS
- Supabase
- GitHub Actions
- Build + Test + Deploy
- E2E Tests (Playwright)
Goals & Requirements
Personal Showcase
Present myself to potential employers and recruiters.
Skills Demonstration
Document the building process as part of the showcase — architecture, decisions, and progress.
Technology Playground
Experiment with new technologies in a real, deployed project.
- Event sourcing
- Free Cloudflare Pages
- Supabase ecosystem
- Temporal
Non-Functional Requirements
| Automation | Minimal manual overhead. Infrastructure as code or GitHub Actions for everything. |
| Reliability | The site must be up reliably. |
| Cost | As cheap as possible — preferably free. Backend hosting: Oracle Cloud Always Free (fallback: Hetzner VPS ~€4/month). |
Architecture Overview
Architecture Decisions
Key technical decisions documented as Architecture Decision Records.
Backend Hosting → Oracle Cloud
Oracle Cloud Always Free ARM A1 instance, with Hetzner VPS as fallback.
Backend Hosting → Oracle Cloud
Oracle Cloud Always Free ARM A1 instance, with Hetzner VPS as fallback.
Context
Need a host that supports long-running processes (ASP.NET Core + Temporal background workers). Most free tiers are serverless/scale-to-zero and shut down the process when idle.
Decision
Start with Oracle Cloud Always Free ARM A1 instance (4 OCPUs, 24 GB RAM, 200 GB storage). If Oracle proves unreliable, fall back to Hetzner VPS (~€4/month).
Why
- Free tier with generous specs (4 OCPUs, 24 GB RAM, 200 GB storage)
- Supports long-running processes and Docker
- Upgrading to PAYG (still free) disables idle reclamation
Known Risks
| Risk | Details | Mitigation |
|---|---|---|
| Signup rejection | Oracle rejects many signups based on region demand and payment method | Use real credit card, match billing address, pick less popular region |
| ARM capacity | Popular regions often have no A1 capacity available | Choose EU or less popular region at signup |
| Idle reclamation | Instances with <20% CPU at P95 over 7 days are stopped after 1 week notice | Real workloads stay above threshold; PAYG disables this entirely |
| Account suspension | Rare reports of accounts suspended without clear reason | Keep PAYG enabled; maintain real usage |
Setup Notes
- Oracle Linux 8 aarch64 — ships with Podman out of the box, optimized for Oracle Cloud hardware
- Set a large boot volume (~150 GB) at creation to avoid iSCSI block volume complexity
- Open ports in both OCI security list and OS firewall
Alternatives Considered
Frontend Framework → Astro SSG
Astro in SSG mode with Vue components hydrated only where needed.
Frontend Framework → Astro SSG
Astro in SSG mode with Vue components hydrated only where needed.
Context
Need a frontend framework for a mostly-static site hosted on Cloudflare Pages. Requirements: Tailwind CSS, i18n (Czech/English), progressive interactivity, minimal JS shipped to users.
Decision
Astro in static site generation (SSG) mode. Interactive components use vanilla JS, with the option of Vue islands for complex UI logic. No Cloudflare Workers — Pages serves plain HTML/CSS/JS from CDN.
Why
- Free forever — SSG on Cloudflare Pages has no per-request cost, unlike SSR with Workers which bills per invocation
- Every page is pre-built and cached on CDN edge nodes worldwide — near-instant loading
- Ships zero JS by default — only interactive islands include client-side code
- Built-in i18n routing and locale detection
- Cloudflare acquired Astro (Jan 2026) — best-in-class Pages integration
Alternatives Considered
Event Sourcing → Marten on PostgreSQL
Marten framework on PostgreSQL instead of a custom implementation on MongoDB.
Event Sourcing → Marten on PostgreSQL
Marten framework on PostgreSQL instead of a custom implementation on MongoDB.
Context
Event sourcing was a given — the question was whether to build it from scratch on MongoDB or use an established framework. This is the first event sourcing implementation, so learning curve and speed matter.
Decision
Use Marten on PostgreSQL — a mature .NET event sourcing framework that runs on the same database we already have.
Why
- Already experienced with SQL but not MongoDB — faster to get up and running
- No extra infrastructure — reuses the existing Supabase PostgreSQL, no need to run MongoDB
- Inline snapshot projections maintain read models with zero extra code
- Mature framework with good documentation — faster to learn than building from scratch
- Wanted to try Supabase ecosystem — that meant PostgreSQL, and Marten fits naturally
Alternatives Considered
Auth → Supabase Auth
Use Supabase's built-in auth instead of building custom or using a separate provider.
Auth → Supabase Auth
Use Supabase's built-in auth instead of building custom or using a separate provider.
Context
The app needs to act as an OAuth provider — users will authenticate via OAuth when connecting through MCP. Also needs standard email/password login.
Decision
Use Supabase Auth — it's already part of the Supabase ecosystem we chose for the database, supports OAuth out of the box, and runs locally in Docker for development.
Why
- Already using Supabase for the database — auth comes included, no extra service to manage
- Supports acting as an OAuth provider — needed for MCP, where users authenticate with the app via OAuth
- Runs locally in Docker — full auth flow works offline without external dependencies
Alternatives Considered
Code Organization → Vertical Slices
Code organized by feature, not by technical layer.
Code Organization → Vertical Slices
Code organized by feature, not by technical layer.
Context
Need a backend code organization strategy that keeps related code together and avoids the "change one feature, touch five layers" problem.
Decision
Code is organized by feature (e.g., Features/JobOffers/) rather than by technical layer. Each feature folder contains its controller, events, DTOs, and handlers.
Why
- Adding a new feature = adding a new folder with all its files, no changes to shared layers
- Each feature is self-contained — easy to understand, review, and test in isolation
- Avoids the typical layered architecture problem where a single change touches Controllers/, Services/, Models/, DTOs/, etc.
Testing Strategy
Mock as little as possible. Real database everywhere, full-stack E2E with working auth.
Testing Strategy
Mock as little as possible. Real database everywhere, full-stack E2E with working auth.
Context
Need a testing approach that provides high confidence with minimal mocking, covering the full stack from API endpoints to database.
Decision
Three layers of testing, all running in parallel in CI/CD. Each blocks deployment on failure.
Why
- Backend integration tests (main focus): xUnit + Testcontainers — real PostgreSQL, full API via WebApplicationFactory. Mock as little as possible (auth is mocked, external services like Slack/email will be mocked), but the database is real so most functionality is easy to assert
- Backend unit tests: for domain logic and pure functions where integration tests would be overkill
- Frontend page tests: Playwright — builds the static site, verifies rendering, navigation, i18n, and dark mode
- E2E tests: Playwright against the full running application — working auth, working DB, everything real. Only external systems like Slack and email can't be asserted
- CI/CD: All three test suites run in parallel, all block deployment on failure
Development Roadmap
6 versions from static site to advanced backend features. Each version adds a layer of complexity.
Simple Site
~7 hours · Mar 27, 2026
Simple site with domain, hosting and deployment pipeline.
What was done
-
Set up domain, DNS and Cloudflare Pages hosting -
Designed pages in Google Stitch, built with Astro + Tailwind
-
GitHub Actions deploy pipeline
-
Light and dark theme with system preference detection
-
Language picker (English / Czech) with per-page translations
Backend Included
~25 hours · Mar 28 – Apr 1, 2026
Could do it in ~10 hours next time.
Time tracked covers the initial rollout only. Improvements and iteration add significantly more.
ASP.NET Core backend with Supabase Auth and Marten event sourcing. Used for managing job offers — users can submit and track offers, admins can review and update statuses.
What was done
-
Supabase project and Google OAuth setup -
Oracle Cloud VM, PostgreSQL, Caddy reverse proxy -
ASP.NET Core Web API with vertical slices and Marten event sourcing
-
Supabase Auth with email/password + Google OAuth, account linking and avatar upload
-
Job offer form with file upload, user dashboard and admin view
-
Docker setup, CI/CD pipeline to Oracle Cloud with blue/green deployment
-
Integration tests (Testcontainers) and E2E tests (Playwright)
-
SEO: sitemap, robots.txt, llms.txt, styled 404 page
-
Optimizing CI/CD pipeline -
Improving run configs for DevX -
Restructuring code a few times -
Cloudflare Turnstile CAPTCHA on job offer submission
Emails, Slack & Observability
Email confirmations and Slack notifications on job offer submissions and status changes. Full observability stack: Sentry for errors, traces, and logs, PostHog for product analytics.
Background Tasks
Move emails and notifications into background processing. Self-hosted Temporal on the backend VPS with retry semantics. Fallback: Azure Queue Storage.
MCP Server
Expose job offer management via a Model Context Protocol server. Users can submit, track, and manage their job offers through any MCP-compatible client.
Pay to Win (Stripe)
Monetize job offer submissions via Stripe with tiered pricing.
| Tier | Price | Guarantee |
|---|---|---|
| Free | $0 | Response within 7 days |
| Premium | $5 | Response within 24 hours |
| Interview (30 min) | $25 | Call within 7 days |
| Interview (1 hour) | $50 | Call within 7 days |
Lessons Learned
Opinions formed after building and shipping with these technologies.
Using Supabase
Using Supabase
Verdict
Would probably do it again just for the setup simplicity on personal projects. Not on commercial projects with real budgets.
Observations
- Free PostgreSQL is cool and worth it on its own
- Auth has some rough edges — e.g. email provider issues ( see discussion )
- File storage is cheap everywhere — S3 or Azure Blobs are just as cheap and I'd trust them more than Supabase Storage
- Using Supabase slows down the dev CLI compared to just using a plain PostgreSQL Docker image
- There's no real pressure to use the bundled file storage and auth — but you kinda feel like an idiot if you don't, since it's right there
Using Event Sourcing / Marten
Using Event Sourcing / Marten
Verdict
Would probably write some custom stuff next time rather than using Marten as-is.
Observations
- Wouldn't write 100% clean event sourcing — instead, every update would also store a change record containing the previous values
- Storing previous values goes against ES philosophy, but allows pagination of history records — which is more practical
- Would use standard SQL storage for projections rather than JSON — this also enables more granular concurrency controls. Marten only offers two extremes: append everything without checks, or crash if anything has changed. No middle ground
- Events would be stored in the same transaction into another table, then moved to cheaper storage (e.g. Azure Table Storage) via an outbox pattern once SQL size becomes an issue
Astro SSG
Astro SSG
Verdict
Would probably go for an SSR framework next time.
Observations
- Already had places where a router would help — e.g. shared header, footer, and auth status across pages
- SSG is simpler for deploy setup, but in real circumstances you just pay for the infrastructure and don't worry about it
Witness the progress.
This is a public engineering journal. Follow the repository to see the commits that built this exact page.
View Source Code