IT Manager — North Support
2024 - Present
Manage full business operations including server installation, network health checks, and maintaining client websites and email services. Custom Python automation for email migration.
Network Tech / Manager & Developer · South Africa
IT manager and full-stack developer based in South Africa. I build secure, resilient networks and high-performance web and mobile software with React, React Native, Next.js, and Python — bridging the gap between infrastructure and code.
Nick Loubser is an IT manager and full-stack developer based in South Africa. Since 2021 he has built web and mobile applications with React, React Native, Next.js, Vue, Nuxt, TypeScript, and Python for Northroom Technologies / Evolver Systems, and since 2024 he manages business IT operations at North Support — covering server installation, network health, client websites, and email services.
He publishes open-source tooling under the ShadowsKeep organisation on GitHub, including Python email-migration and backup systems used in production and AI developer tooling such as AMP, a package manager for MCP servers. He is available for remote freelance and contract work worldwide.
2024 - Present
Manage full business operations including server installation, network health checks, and maintaining client websites and email services. Custom Python automation for email migration.
Jan 2023 - Present
Full-stack web and mobile development. Tech stack: React.js, React Native, Node.js, TypeScript, MobX, Supabase, MySQL. Focus on responsive design and system architecture.
Jan 2023 - Jan 2023
Focused on mobile application development and responsive web design during transition from internship.
Jan 2022 - Jan 2023
Advanced internship role focusing on Git work flows, video editing, and mobile application development.
Mar 2021 - Jan 2022
Initial internship gaining experience in development workflows, version control, and basic mobile development.
A multiplayer horror murder mystery Discord Activity — up to 10 players in a voice channel investigate an AI-generated murder case with unique story, scene images, and voice narration every game (Gemini), on Cloudflare Workers + Durable Objects.
Built with: TypeScript, Discord SDK, Cloudflare Workers, Gemini AI
A Rust CLI that works like npm for MCP servers: install an AI skill once and AMP writes the correct config for every AI client on your machine — Claude Code, Cursor, VS Code, JetBrains, and more.
Built with: Rust, CLI, MCP
A VS Code extension that visualizes an entire TypeScript/React codebase as an interactive dependency graph — components, hooks, types, and call graphs — with AI-ready context export for coding assistants.
Built with: TypeScript, VS Code Extension, React
A comprehensive Python terminal dashboard for email migration, backup, restore, and synchronization with a modern, interactive interface built with Textual.
Built with: Python, Textual, TUI
A production-ready Python tool for migrating emails between IMAP accounts with a beautiful Rich TUI dashboard and Discord webhook notifications.
Built with: Python, Rich, IMAP
A bespoke stopwatch application for Parkrun events, built with Nuxt 3 and Vue.js for real-time tracking.
Built with: Vue, Nuxt 3, TypeScript
An advanced timekeeping solution for Parkrun, leveraging Nuxt 3 and Edge capabilities for high performance.
Built with: TypeScript, Vue, Edge Functions
A comprehensive, production-ready email backup and transfer system with web interface, user authentication, and advanced security features.
Built with: Python, Flask, Bootstrap, IMAP
Email: [email protected]
GitHub: https://github.com/NICKLOUBSER · https://github.com/ShadowsKeep
LinkedIn: https://www.linkedin.com/in/petri-loubser-493676192/
Nick Loubser · July 2026 · Source on GitHub
Horror Quest is a multiplayer horror murder mystery that runs inside a Discord voice channel. Up to 10 players investigate a procedurally generated murder case together: every game has a unique AI-generated story, suspects, clues, and red herrings, with fresh scene artwork each chapter and optional voice narration. The whole thing runs serverless on Cloudflare Workers and costs about $0.10 per full game session.
A Discord Activity is a web app embedded directly inside a Discord voice channel via an iframe, using the @discord/embedded-app-sdk. Everyone in the channel shares the same session — which makes Activities a natural fit for party games, but also means you inherit an unusual runtime: your app lives inside Discord's sandbox, authenticates through Discord's OAuth2, and every network request is proxied through Discord's content security rules.
| Layer | Technology |
|---|---|
| Hosting + API | Cloudflare Workers (Hono router) |
| Multiplayer state | Durable Objects, SQLite-backed |
| Real-time sync | WebSockets with hibernation |
| Story generation | Gemini 2.5 Flash |
| Scene images | Imagen 3.0 Fast |
| Narration | Gemini 2.5 Flash TTS ("Charon" voice) |
| Frontend | Vite + TypeScript, no framework |
One Worker serves the static frontend, exposes the API, and upgrades WebSocket connections. Each Discord channel maps to exactly one Durable Object instance (idFromName(channelId)), which is the single source of truth for that room's game state — players, votes, chapters, clues. No separate game server, no Redis, no sticky sessions: the platform guarantees one instance per room globally.
The core loop is vote-gated: each chapter presents choices, and the story only advances when every connected player has voted. The Durable Object holds the vote tally and broadcasts state over WebSockets. Because Durable Objects support WebSocket hibernation, the object can be evicted from memory between messages while connections stay open — you pay for compute only in the moments someone actually votes. A 3-minute chapter timer creates urgency, but it's deliberately visual-only: the game waits for all votes, because getting kicked out of a story beat by a timer feels terrible in a group setting.
Three models cooperate per game:
Story. Gemini 2.5 Flash generates the full mystery scaffold — setting, suspects, the actual culprit, clues and red herrings — then writes each chapter in response to the group's votes, keeping earlier clues persistent so the mystery stays solvable.
Images. Imagen 3.0 Fast renders a new scene image every chapter, displayed with a Ken Burns pan/zoom so static images feel alive.
Narration. Gemini TTS reads scenes aloud on demand. TTS requests route through the Durable Object so the generated audio is cached per chapter — ten players clicking "Listen" costs one generation, not ten. That single routing decision is most of the cost model.
Things that are trivial on the open web need care inside an Activity:
OAuth. The SDK's authorize() hands the client a code; the Worker exchanges it server-side so the DISCORD_CLIENT_SECRET never ships to the browser.
Autoplay. Narration audio needs Permissions-Policy: autoplay=(self) and a relaxed Cross-Origin-Embedder-Policy on every response, or Discord's iframe silently blocks playback.
URLs. All traffic goes through Discord's proxy under URL mappings you configure in the Developer Portal — local development means tunneling your dev server (cloudflared) and pointing the Activity at the tunnel.
| Item (per ~8-chapter, 4-player game) | Cost |
|---|---|
| Imagen 3.0 Fast — 8 scene images | ~$0.08 |
| Gemini 2.5 Flash — full story | ~$0.002 |
| Gemini TTS — per cached "Listen" | ~$0.01 |
| Cloudflare Workers + Durable Objects | free tier |
| Total | ~$0.10 |
A $10 API budget cap runs 100+ complete group games. Cost ceilings were a design input, not an afterthought — image count per chapter, TTS caching, and model tier were all chosen against that number.
Durable Objects are the right primitive for room-based multiplayer: one object per room deletes a whole category of distributed-state problems. Cache AI output at the room level, not the client level. And design for the sandbox you actually run in — half the engineering in a Discord Activity is respecting Discord's iframe, proxy, and OAuth constraints rather than fighting them.
Nick Loubser · April 2026 · Source on GitHub
Every AI client keeps its MCP configuration in a different JSON file, in a different format, in a different place. Adding one MCP server to Claude Code, Cursor, VS Code, and JetBrains meant editing four config files by hand — and keeping them in sync forever. AMP is a Rust CLI that treats MCP servers like packages: amp add filesystem once, and every client on the machine is configured.
MCP (Model Context Protocol) is how AI clients talk to external tools. It won the standards race — but each client adopted it with its own config schema and location. The result is the same server definition duplicated across four or more JSON files. When a server's args change, most people find out one client at a time.
The mental model that fixes this already exists: a package manager. npm doesn't ask you to hand-edit resolved dependency paths; you declare what you want and the tool writes the machine-specific plumbing.
Per-project: amp init creates an amp.json in the repo, and amp add filesystem records the dependency there and writes the project-level config files each client reads. Commit amp.json and a teammate's amp install reproduces the exact same AI tooling — the way package.json made Node projects reproducible.
Global: amp add -g brave-search injects the server into every AI client's user-level config on the machine. AMP knows each client's format and location — Claude Code, Cursor, VS Code, JetBrains, and more — so one command updates them all consistently.
A config-rewriting tool has exactly one job requirement: never corrupt the files it touches. Rust's type system and error handling make the failure modes explicit, and the compiled binary starts instantly — this is a command you run dozens of times a day.
Distribution matters as much as the tool: installers for macOS/Linux (curl | bash) and Windows (PowerShell) download pre-built binaries and add them to PATH — no Rust toolchain required. Building from source with cargo install stays available for people who want it.
When an ecosystem grows N clients that each invent their own config, the fix is rarely standardising the clients — it's one tool that owns the translation. And borrowing a familiar mental model (npm's project/global split) meant zero learning curve: everyone who has used a package manager already knows how AMP works.
Nick Loubser · January 2026 · Source on GitHub
Shadow Map is a VS Code extension that turns a TypeScript/React codebase into an interactive dependency graph — components, custom hooks, interfaces, functions, and the relationships between them. It exists for two audiences at once: developers trying to understand an unfamiliar architecture, and AI coding assistants that need deep project context to give accurate answers.
Most dependency visualizers stop at import statements. Shadow Map parses the code itself: React components with their props, state, and hooks metadata; custom hooks detected and categorized as their own node type; HOC patterns; memo/lazy/forwardRef optimization wrappers; Context providers and their consumers; and full call graphs between functions.
On TypeScript it goes equally deep — interfaces, type aliases, enums, generic parameters, async and generator functions. The practical difference: on the same codebase, Shadow Map surfaces roughly 3x more code elements than basic tools (60+ nodes where an import-graph shows 19), because most of what matters in a React app isn't visible at the import level.
The second output format is for machines. AI assistants answer architecture questions badly when their context is a handful of open files — they can't see that a component consumes a Context three levels up, or that a hook is reused in eleven places. Shadow Map exports its graph as structured, AI-ready context: a compact description of the project's real shape that fits in a prompt.
That turns "paste some files and hope" into "hand the assistant a map." Questions like what breaks if I change this hook's return type? become answerable, because the call graph is in context.
Code structure is data, and it has more than one consumer now. Building the visualization and the AI export from the same analysis pass meant every parser improvement paid out twice — better graphs for people, better context for models.
Nick Loubser · December 2025 · Source on GitHub
Shadows Mail Tools is the tool I wished existed after one too many manual mailbox migrations. It's a full-screen terminal dashboard (built with Textual) that handles migration, backup, restore, and synchronization between IMAP servers — with the reliability features that real client migrations demand: checkpoint/resume, rate-limit detection, and encrypted credential storage. I use it in production for actual client work.
The dashboard drives four modes: migrate between IMAP servers with folder structure preserved, backup to Maildir++ on disk, restore from backup with full metadata, and sync (one-way or incremental) between live accounts. Folder-mapping presets translate Gmail, Outlook, and Yahoo folder conventions automatically, and domain batch processing runs multiple accounts grouped by domain in parallel — the shape of a real business migration, where you move a whole company's domain, not one inbox.
IMAP migrations fail constantly and boringly: servers throttle you, connections drop at message 40,000, transient errors look like fatal ones. The design assumes all of it. Every operation checkpoints its state automatically and resumes where it stopped after an interruption. Errors are classified — transient, permanent, or rate-limit — and only the right class triggers exponential-backoff retries. Rate limiting is detected and handled rather than hammered through. Discord webhooks report per-account completion, batch completion, and errors, so an overnight run doesn't need babysitting.
A migration tool holds the keys to every mailbox it touches, so credential handling got its own subsystem: a multi-source provider that loads from environment variables, encrypted files (Fernet/AES-256), interactive prompts, or the Windows Credential Manager via keyring — plus OAuth2 flows for Gmail, Outlook, and Yahoo instead of app passwords where possible. Configuration is validated YAML with CLI overrides, and everything runs on both Windows and Linux.
The dashboard is the least important part. What makes a migration tool production-grade is everything around the transfer loop: resumability, error classification, and treating credentials as radioactive. Textual just made the reliable core pleasant to watch.
Nick Loubser · December 2025 · Source on GitHub
Shadow Mail Gate does one thing: move email between IMAP accounts, reliably, while showing you exactly what's happening. It's fully async Python 3.12 with a live Rich dashboard — progress, statistics, and errors updating in real time — and Discord webhook notifications for the hours when nobody is watching the terminal.
IMAP migration is embarrassingly I/O-bound: almost all wall-clock time is spent waiting on two mail servers. The entire pipeline is async/await, so multiple accounts and folders move concurrently instead of queuing behind each other's network latency. For multi-account jobs that's the difference between an afternoon and a weekend.
First run is an interactive wizard — cli.py interactive — that walks through source and destination accounts, folder selection, and optional Discord webhook, then saves a YAML config. After that it's run --config config.yaml, with a check command that validates a config without touching a mailbox. The wizard exists because config-file-first tools get their config wrong on the first try, every time, usually against a production mail server.
The Rich TUI shows live per-account progress, transfer statistics, and an error feed. Discord notifications fire on start, completion, errors, and periodic checkpoints. Graceful shutdown, structured logging, and deliberate error handling round it out — this is the sibling of Shadows Mail Tools, sharing the same production-first philosophy in a leaner, single-purpose package.
A focused tool with excellent visibility beats a flexible tool you can't observe. The dashboard and the webhooks aren't decoration — for long-running migrations, knowing precisely where a run is (and being pinged when it isn't) is the feature.
Nick Loubser · January 2026 · Source on GitHub
The Shadows Keep Backup System is the web-app evolution of my email tooling: a Flask application that backs up, browses, and transfers email from any IMAP provider — Gmail, Outlook, Yahoo, ProtonMail, iCloud, anything — behind a proper multi-user login, with backups flowing to local disk or 70+ cloud storage providers. It's built for cloud deployment and runs as a hosted service, not a script.
My terminal migration tools assume the operator is technical. This system assumes they aren't: a Bootstrap web dashboard shows every backup with statistics, an email viewer browses archived mail with full content and attachments, and transfers support dry-run mode so you can see what would happen before it does. Authentication is real — bcrypt-hashed passwords, Flask-Login sessions, and admin/user roles with an admin interface for managing accounts.
Email archives are where text encoding goes to die. A large share of the codebase handles exactly that: surrogate characters, broken encodings, attachment filenames that are illegal on Windows, and metadata preservation — headers, flags, timestamps — through every backup and restore cycle. Cloud storage works through a single abstraction over 70+ providers (S3, Google Drive, Dropbox, OneDrive, MEGA, pCloud, Box, Proton Drive…), in every direction: direct-to-cloud backup, local-to-cloud upload, cloud-to-local restore, with connection testing before any job runs.
Because the system already parses every message, it analyzes them too: domain and display-name spoofing detection, suspicious-pattern analysis, and a 0–100 risk score per email with actionable recommendations. An archive you can audit for the phish that started an incident turns backup storage into a security tool.
Wrapping working CLI logic in a web product is mostly not about the web part — it's authentication, multi-tenancy, encoding resilience, and giving non-technical users safe defaults like dry-run. The phishing scoring was almost free once the parser existed, and it's the feature people remember.
Nick Loubser · August 2024 · Source on GitHub
Parkrun events need a clock that everyone involved can trust and read at a glance. Parkrun Stopwatch is a deliberately small Nuxt 3 app that does exactly that: a large-format race timer with a date picker for event day, styled for outdoor readability. It was also the project that kicked off my Parkrun timing tooling — and the itch that later grew into a full timing system.
The whole app is a Timer component, a DatePicker component, one page, and a Tailwind layout with Montserrat loaded locally. No backend, no accounts, no state beyond the running clock. A phone stopwatch technically does this — but a fullscreen browser timer on any borrowed laptop or tablet, with typography sized for volunteers to read in sunlight, fits how an actual event runs.
This was my proving ground for the Nuxt 3 + Vue composition API stack — and for the discipline of stopping at the right scope. The unanswered questions it raised (how do you record finishers? how do runners get their times?) became the spec for Parkrun Timekeeper, the full-stack successor. Sometimes a project's best output is the roadmap for the next one.
Nick Loubser · September 2025 · Source on GitHub
Parkrun Timekeeper is the full-stack answer to the question the stopwatch left open: what happens after the clock starts? It's a complete event timing system running server-side on Cloudflare's edge via Nuxt and NuxtHub — timekeepers record finishes as runners cross the line, results stream live over WebSockets, and runners claim their own finish times afterwards.
The server API models a race day end to end: events are created against locations, started and stopped (/api/events/[id]/start, /stop), and accumulate finish records posted by the timekeeper interface as each runner crosses the line. Authentication protects the operational endpoints, and a database on the edge platform holds events, runners, locations, and finish records. The frontend maps the same lifecycle: an events dashboard, a timekeeper view with the timer and a FinishRecorder, a live results page per event, and a results archive.
Each event exposes a WebSocket endpoint (/api/events/[id]/ws), so spectators' phones update the moment a finish is recorded — no polling, no refresh. Edge hosting earns its keep here: the same deployment serves the SSR pages and holds the socket connections close to the event, with nothing to provision.
Timekeeping records positions and times, not identities — pressing a button per finisher is the only thing fast enough at the line. Identity comes later: runners visit the claim page and attach themselves to their finish record (/api/finish-records/[id]/claim). Decoupling those two steps is the difference between a system that works in a demo and one that works with eighty sweaty runners.
This project was also a workflow experiment: it's built spec-first, with a project constitution, feature specs, and plan/task templates driving AI-assisted implementation. The lifecycle-shaped API above came out of writing the spec before the code — the clearest evidence yet, for me, that the spec is where the architecture actually happens.