A booking website and admin panel for a tattoo studio, built with ASP.NET Core and Blazor Server on .NET 10.
Live demo · Built solo by Shine Chikwapulo
Status: Portfolio project, designed around a tattoo studio in Hout Bay. It has not been presented to the studio yet and holds no real customer data. The hosted demo uses SQLite and local file storage on Render, so data can reset when the service redeploys.
For clients
- Browse artists, services, a gallery and reviews.
- Submit a booking request through a validated form and receive a confirmation email.
- Use a private link to view the booking's status, request a new date or cancel it. Links use a 256-bit random token and expire after 30 days.
For the studio (admin area)
- Cookie-authenticated admin with passwords hashed by ASP.NET Core's password hasher.
- Pages for the dashboard, bookings (and booking detail), a calendar, artists, services, reviews, messages and site settings.
- Image uploads for artist and gallery content, checked for extension, size (5 MB) and file signature.
| Area | Technology |
|---|---|
| Framework | .NET 10, ASP.NET Core, Blazor Server, Razor Pages (login) |
| Data | Entity Framework Core 10, SQL Server (development), SQLite (demo deployment) |
| Resend HTTPS API, with a console fallback when no API key is set | |
| Packaging | Docker (multi-stage build, non-root user), Docker Compose for local SQL Server |
| Hosting | Render |
| Tests | xUnit |
The solution has four projects plus a test project:
| Project | Responsibility |
|---|---|
Inkrepublik.Web |
Blazor components, the admin login pages, startup and dependency injection |
Inkrepublik.Domain |
Entities and enums (bookings, artists, services, reviews and so on) |
Inkrepublik.Data |
DbContext, EF Core migrations (SQL Server), idempotent database seeder |
Inkrepublik.Services |
Booking, admin, email and file-storage logic behind interfaces |
Inkrepublik.Tests |
xUnit project |
On startup the app applies migrations (SQL Server) or creates the schema (SQLite), then seeds sample data. The admin account is seeded only when both Admin:Email and Admin:Password are set.
Prerequisites: .NET 10 SDK, Docker Desktop (for local SQL Server).
git clone https://github.com/ShyneChikwapulo/inkrepublik.git
cd inkrepublik
# 1. Start SQL Server 2022 in Docker (port 1433)
docker compose up -d
# 2. Set an admin password (stored in .NET user secrets, not in the repo)
dotnet user-secrets set "Admin:Password" "<choose-a-strong-password>" --project src/Inkrepublik.Web
# 3. Run the app
dotnet dev-certs https --trust # first time only
dotnet run --project src/Inkrepublik.Web --launch-profile httpsOpen https://localhost:7112. The admin login is at /admin/login, using the email from Admin:Email (the Development settings use admin@inkrepublik.local) and the password you set.
No Docker? Use SQLite instead:
DatabaseProvider=Sqlite dotnet run --project src/Inkrepublik.Web --launch-profile httpsEmail. Without a Resend API key, emails are written to the console log. To send real email:
dotnet user-secrets set "Resend:ApiKey" "re_..." --project src/Inkrepublik.WebResend's sandbox sender (onboarding@resend.dev) only delivers to the address you signed up with.
Set these as user secrets, appsettings values or environment variables (use __ instead of : in environment variables).
| Key | Purpose |
|---|---|
DatabaseProvider |
SqlServer or Sqlite |
ConnectionStrings:DefaultConnection |
SQL Server connection string |
DatabasePath |
SQLite file path |
Resend:ApiKey, Resend:FromAddress, Resend:FromName |
Email via the Resend API |
Studio:OwnerEmail |
Where new-booking notifications go |
Studio:PublicBaseUrl |
Base URL used in emailed booking links |
Admin:Email, Admin:Password |
Admin account created on first start |
dotnet testThe test project currently contains only a placeholder test. Real tests are on the roadmap.
docker build -t inkrepublik .
docker run --rm -p 8080:8080 \
-e ASPNETCORE_ENVIRONMENT=Production \
-e DatabaseProvider=Sqlite \
-e DatabasePath=/app/data/inkrepublik.db \
-e Admin__Email=admin@example.com \
-e Admin__Password='<choose-a-strong-password>' \
-e Studio__PublicBaseUrl=http://localhost:8080 \
inkrepublikThe image exposes port 8080 and runs as a non-root user. /app/data (database) and /app/wwwroot/uploads are writable. Mount volumes there if you need data to survive restarts.
- Unit and integration tests for booking rules, token handling and admin authorisation
- Login hardening: validate the return URL, add rate limiting or lockout, mark the auth cookie
Securein production - Persistent production storage (PostgreSQL and object storage for uploads)
- Facebook gallery sync (planned, not built)


