Project Use-Case
This project was started to help me regain my technical edge when applying for engineering roles after spending close to five years in non-engineering positions.
One of the key requirements was to include as many aspects of engineering as possible. I achieved this by building a complete full stack hosted on a VPS, forcing me to learn modern hosting practices, manage my own data layer, and ensure secure communication between the backend and frontend.
Project Challenges
The main challenges were managing multiple authentication systems, getting up to speed with active threats, and preventing frequent spam posts from bots.
These were solved through a fair amount of trial and error (especially around authentication). It’s not 100% perfect, but it now fails gracefully rather than exposing vulnerabilities. Spam posting is mitigated by requiring accounts for commenting, and accounts must be activated before users can interact with the site.
Bots hammering known vulnerabilities (e.g. wp-admin, generic PHP requests, etc.) are handled at the Nginx level by dropping those requests before they reach the application.
Another major challenge was search optimisation and link sharing. Platforms like LinkedIn need to discover images, titles, and metadata, which is difficult when the frontend is 100% JavaScript.
This was solved by introducing an og/ endpoint that returns the required metadata for crawlers, while human users are redirected to the actual content.
Project Technical Stack
Frontend — carlsson-tech-ui
| Component | Technology |
|---|---|
| Framework | Angular 21 (standalone + module hybrid) |
| Language | TypeScript 5.9 |
| UI Libraries | Bootstrap 5, NgBootstrap 20, UIKit 3 |
| Authentication | MSAL (Microsoft), Auth0 JWT, angular-oauth2-oidc |
| Markdown Rendering | ngx-markdown, Prism.js |
| HTTP | Angular HttpClient with JWT interceptor |
| State Management | RxJS 7 |
Backend — carlsson-tech-api
| Component | Technology |
|---|---|
| Framework | ASP.NET Core 9 |
| Language | C# (.NET 9) |
| ORM | Entity Framework Core 9 |
| Database | Microsoft SQL Server |
| Authentication | JWT Bearer, hand-rolled OAuth (GitHub, Microsoft, LinkedIn) |
| Image Processing | SixLabors.ImageSharp 3 |
| Resend API | |
| Captcha | hCaptcha |
| API Documentation | Swagger / OpenAPI (Swashbuckle) |
| Containerization | Docker (multi-stage build) |
Architecture
The platform follows a client–server architecture with a clear separation between a stateless single-page frontend and a stateful RESTful backend API.
Frontend (carlsson-tech-ui)
The Angular SPA communicates exclusively with the backend via versioned REST endpoints (/v1/) over HTTPS. A JWT interceptor automatically attaches Bearer tokens to outbound requests, while a dedicated error interceptor handles 401/403 responses.
The application is organised into domain-scoped feature modules covering authentication, blog authoring, comments, user profiles, admin tooling, and media management. The build outputs a static asset bundle served independently (e.g. via Nginx or a CDN).
Backend (carlsson-tech-api)
The API follows a classic layered pattern: controllers delegate to business logic, which interacts with the database through an EF Core ApplicationDbContext.
Key design decisions include:
- Authentication: JWT access tokens (1-hour expiry) paired with database-tracked refresh tokens for rotation. OAuth flows for GitHub, Microsoft, and LinkedIn are implemented natively without a third-party identity framework.
- Authorisation: A role-based access control model using a
GroupPermissionbitmask enum. Permissions are enforced via a custom[RequirePermission]attribute on controller actions. Seven permission tiers exist, ranging from Guest to Admin. - User model: Table-per-Type (TPT) inheritance distinguishes
LocalUser(email/password) fromOAuthUser(provider + subject ID) within the same user hierarchy. - Media: Blog images and user avatars are processed server-side using ImageSharp (crop, resize, format conversion) and stored on the file system under
wwwroot. - Content moderation: A reporting system allows posts and comments to be flagged, with a dedicated review workflow for moderators.
- CORS: Restricted to
*.carlsson.techsubdomains and localhost.
Data Layer
A single SQL Server Express instance hosts the application database, covering users, posts, comments, tags, reports, projects, and questionnaires. EF Core migrations manage schema evolution.
Server Stack
Nginx acts as the sole public-facing entry point, handling TLS termination (Let's Encrypt certificates with Diffie-Hellman key exchange), HTTP-to-HTTPS redirects, and www-to-non-www canonicalisation.
Beyond routing, Nginx also provides an active layer of threat mitigation:
- Vulnerability probe blocking: Requests targeting common attack surfaces — WordPress paths (
/wp-admin,xmlrpc.php), PHP files, phpMyAdmin panels, backup file extensions (.bak,.sql,.dump), and exposed dotfiles/directories (.git,.ssh,.aws,.htaccess) — are terminated with a404before reaching the application. - Sensitive file protection: Requests for
.json,.config,.env,.log, and.inifiles are denied outright. - IP blocklist: A dynamically generated
blockips.confis included at runtime, maintained by the IP management layer and reloaded viasystemctl reload nginxwithout service interruption. - Static asset caching: Frontend assets (JS, CSS, fonts, images) are served with a one-year
Cache-Controlheader, reducing load on the application. - Sitemap proxying with cache:
/sitemap.xmlis generated dynamically by the backend and proxied through Nginx, which caches the response for 24 hours. To the client, it behaves like a static file. - OG preview redirect: Requests to
/read/:slugfrom known social media crawlers (LinkedIn, Twitter, Facebook, Discord, Slack, Google) are redirected to the API’s/v1/blog/og/:slugendpoint, which returns pre-rendered Open Graph metadata. Human visitors are served the Angular SPA as normal. - SPA fallback: All unmatched routes fall through to
index.html, enabling Angular’s client-side routing.
Deployment
The API is deployed as a Docker container (ASP.NET Core runtime, port 5000). The frontend is deployed as a static SPA build. Configuration is environment-driven via layered appsettings.json files and environment variables, with secrets injected at runtime in production.
Deployments are performed via privileged Bash scripts (deploy-backend.sh, deploy-frontend.sh) run manually over SSH.
Backend deployment
- The API systemd service is stopped cleanly before any files are touched.
- The
wwwrootdirectory (user-uploaded images and assets) is backed up to a timestamped archive to prevent data loss. - The target directory is cleared and replaced with the new publish output.
wwwrootis restored from the backup.- File ownership is set to
www-databefore restarting the service, preventing permission issues. - The service is started and its status is printed immediately.
Frontend deployment
robots.txtandsitemap.xmlare preserved to/tmpbefore the web root is cleared, as they are managed separately from the Angular build.- The new build is copied into place and ownership is set to
www-data. - Preserved files are restored.
The scripts are not exposed to the web. They are executed manually by an authenticated user with sudo access. No deployment tooling, webhooks, or remote execution endpoints are exposed, eliminating that attack surface entirely.