Ahmet Bilal Yazıcıoğlu
Ahmet Bilal Yazıcıoğlu
← Back to blog
September 14, 2026/5 min read/0 viewsen

Building a Database-Free Stealth Admin Portal in Next.js

securitynextjsarchitectureweb
🌐This article is also available in Turkish:
Türkçe Oku →

When I started writing on my personal site, I ran into the exact dilemma most developers face: Do you hook up a heavy headless CMS (Notion, Ghost, Strapi), spin up a full-blown PostgreSQL database with ORMs and user tables just for admin access, or do you resign yourself to writing Markdown files inside VS Code and running git push every single time?

None of those options felt right.

I wanted the freedom to write and edit posts on the go from my phone or any browser in a clean, distraction-free environment. But at the same time, I refused to take on the cost, maintenance overhead, and security baggage of managing a live SQL database. Nor did I want public /admin or /login endpoints exposed to automated web crawlers.

So I designed a custom Stealth Admin Architecture. Here is a breakdown of how it works and how applying defense-in-depth principles made it virtually impervious to automated attacks without a database.

1. Zero Database, Zero SQL Injection (0% Attack Surface)

The most fundamental rule of cybersecurity is simple: You cannot exploit what does not exist.

SQL injection remains one of the most common web vulnerabilities. Attackers probe forms with payloads like admin' OR '1'='1 in hopes of bypassing authentication or dumping tables. But when there is no SQL database running behind the application:

The attack surface drops to zero.

All blog posts on this site live as flat .mdx files under /app/content/blog/*.mdx. In production, this directory is mounted onto a Docker volume. Whenever a post is saved from the studio, it writes atomically to a .tmp file and performs an atomic filesystem swap (fs.renameSync). There are no connection pools to exhaust, no database migrations to run, and no SQL injection vectors.

2. Stealth Routing: Camouflage as a Pure 404

If you run any public-facing server, you already know that automated bots scan common administrative paths around the clock: /admin, /wp-admin, /dashboard, /login.php.

On this site, /admin returns an unconditional 404 Not Found to public internet traffic. As far as any crawler or security scanner is concerned, the route does not exist.

So how do I access the portal?

  1. Dynamic Secret Route ([adminSecret]): The folder name is dynamic. The actual URL path is loaded from an environment variable on the server (e.g. /studio-x88). Because it's evaluated dynamically, no scanner inspecting compiled client-side JavaScript bundles can extract the path.
  2. Access Gate Key (?key=...): Even if someone miraculously guessed the random path and hit bilalyazicioglu.com/studio-x88, the server still returns a cold 404! The login interface only renders when accompanied by a secret query parameter: ?key=my-access-key.

Missing either one leaves the scanner staring at a regular 404 page.

3. Why String Equality Isn't Enough (Timing Attack Mitigation)

In most standard authentication flows, string comparison looks like this:

// DANGEROUS:
if (inputPassword === actualPassword) { ... }

It looks harmless, but it exposes the application to Timing Attacks.

JavaScript engines compare strings character-by-character and short-circuit on the very first mismatch. If the first character matches, the comparison takes a few CPU cycles longer before moving to the next. By measuring microsecond differences in server response times, an attacker can theoretically infer secrets character by character.

To prevent this, all password, username, and PIN checks use Node.js's built-in crypto.timingSafeEqual:

import crypto from "node:crypto";

export function timingSafeCompare(a: string, b: string): boolean {
  if (typeof a !== "string" || typeof b !== "string") return false;
  const bufA = Buffer.from(a, "utf8");
  const bufB = Buffer.from(b, "utf8");
  if (bufA.length !== bufB.length) {
    crypto.timingSafeEqual(bufA, bufA); // Constant-time dummy comparison
    return false;
  }
  return crypto.timingSafeEqual(bufA, bufB);
}

Whether the first character or the last character fails, the server responds in the exact same duration.

4. Multi-Factor Static PIN & Brute-Force Rate Limiting

Reaching the login screen still isn't enough:

  • Username
  • High-Entropy Password
  • Secondary Static Security PIN (A standalone PIN code that works seamlessly without needing to carry a physical authenticator app)

Backing this is an in-memory Rate Limiter. If five failed attempts occur from an IP address, that IP is locked out for 15 minutes (429 Too Many Requests). A brute-force dictionary attack trying millions of combinations is mathematically crippled.

5. Stateless Session Security (HMAC-SHA256 Signed Cookies)

Upon successful authentication, the server sets an admin_session cookie:

  • HttpOnly: Client-side JavaScript cannot read the cookie, neutralizing XSS theft.
  • SameSite=Strict: Protects against CSRF (Cross-Site Request Forgery).
  • Secure: Sent strictly over HTTPS.
  • HMAC Signed: The session payload is signed with a 64-character secret using SHA-256. Tampering with the payload invalidates the signature and drops the session immediately.

"Doesn't Blogging About It Make It Insecure?"

A natural question comes up when writing about internal security defenses: "Doesn't explaining the exact mechanisms in public turn the site into an easy target?"

The answer is an emphatic no.

In cryptography, this was settled back in 1883 by Auguste Kerckhoffs, formalized as Kerckhoffs's Principle (and Shannon's Maxim):

"A cryptosystem should be secure even if everything about the system, except the key, is public knowledge."

This repository is already open source on GitHub (bilalyazicioglu/portfolio). Anyone can inspect the code. But the keys that unlock the doors live strictly in the server's local .env file. An attacker can know every line of code by heart, but without the keys, none of the locks will turn.

Takeaway

By the end of this exercise, I had:

  • Zero database hosting costs and zero connection maintenance,
  • A completely camouflaged admin door that looks like a 404 to the rest of the world,
  • Markdown/MDX editing with live typography preview and drag-and-drop image uploads,
  • And the peace of mind that comes with solid engineering.

Often, good software engineering isn't about adding another database or third-party service; it's about composing simple, well-understood primitives with defense-in-depth to eliminate unnecessary moving parts.

Have thoughts on this? Send me a note.