Learn CodeNotes from sessions with Claude Code, my teacher
Lesson 11

Locking paid features on the server

2026-10-07 · about 6 minutes
securityPHP.htaccesscookiesauthentication

The problem we started with#

The tools site showed some tools as Pro, but the decision "is this person Pro?" was made in the browser, from data the browser itself stored. Two more surprises turned up in review:

  • The sign-up form didn't create an account on the server; it only saved a "free user" in the browser.
  • If a login failed, the page quietly signed the person in as a local "free user" anyway, so any password "worked".
  • The Code Studio Pro link pointed at a folder with no start page, so it showed 403 Forbidden for everyone.

Rule from lesson 09, now put into practice

The browser can display; only the server can decide.

The four Pro tools, four different shapes#

Before locking anything we looked at how each tool reaches the user, because that decides how to protect it:

Tool How it's delivered How we protected it
Meeting Minutes Built into the main page; its AI runs on the server Already protected: the server's AI proxy checks the login
Banner Maker Its own page Page goes through a gate + AI proxy
Code Studio Pro Its own page Page goes through the gate
Remove Background Runs fully in the browser Its code is only delivered by the gate

Protect the thing that has value

For an AI tool the value is the AI call, which happens on the server. For a browser-only tool the value is the code itself, so the code must not be in the public page.

Part 1: the gate#

pro-gate.php is a small script that sits in front of the Pro files:

$p = sgai_verify_token($_COOKIE['sgai_session'] ?? '') ?: sgai_verify_token(sgai_request_token());
$isPro = $p && ($p['plan'] ?? 'pro') === 'pro';
if (!$isPro) {
    // not signed in → sign-in page; signed in but free → upgrade page
    header('Location: ' . ($p ? './?upgrade=1' : 'account.html#signin'));
    exit;
}
readfile($file);   // only Pro members get here

And a rewrite rule in .htaccess makes sure nobody can skip it by typing the file's address:

RewriteEngine On
RewriteRule ^(banner-maker\.html|SGAi-Code-Studio\.html)$ pro-gate.php?t=$1 [L,QSA]

What a rewrite does

The visitor asks for banner-maker.html; the server quietly runs pro-gate.php?t=banner-maker.html instead. The address bar doesn't change, and there's no way to reach the file directly.

Our logins were tokens kept in the browser's localStorage, sent by JavaScript in a header. That works for fetch() calls, but when you simply open a page, the browser sends no custom headers, so the gate couldn't see who you were.

The fix: at login, the server also sets a cookie:

setcookie('sgai_session', $token, [
  'expires' => $exp, 'path' => '/',
  'secure' => true,     // only over https
  'httponly' => true,   // JavaScript can't read it, so malicious scripts can't steal it
  'samesite' => 'Lax',  // not sent on most cross-site requests
]);

Browsers send cookies automatically with every request to the site, including plain page visits.

People already signed in had no cookie

Anyone who logged in before this change had a token but no cookie, so the gate would have bounced them. Fix: the login script now re-checks the session with the server once per page load, and that check sets the cookie. Lesson: when you add something to a login, think about everyone who's already logged in.

Part 3: moving browser-only code behind the gate#

Remove Background's code (about 5 KB) lived inside the public main page. We:

  1. Cut the function out exactly (by matching its braces) and checked the cut piece still parsed.
  2. Saved it as a private file, delivered only by the gate.
  3. Replaced it in the main page with a small loader that asks the gate for the code with the login token, then runs it, or shows "This is a Pro tool. Sign in or upgrade."

An honest limit

Once a Pro member has the code, they could save it. That's true of every website. What we stopped is everyone else getting it for free.

Part 4: real sign-up#

The sign-up form now calls the server, which:

  • validates the email and password (8+ characters);
  • refuses an email that already has an account;
  • stores a bcrypt hash, plan free;
  • emails a one-time confirmation link (valid 48 hours, stored only as a hash);
  • logs the person in (token + cookie).

The "failed login becomes a local account" code was simply deleted. A wrong password now just says so.

Part 5: testing like an attacker and like a customer#

Who Pro page Remove Background code
Not signed in → sign-in page 401
Opens the private code file directly — 403
Pro member ✅ opens ✅ delivered
Free member → upgrade page 402 Payment Required
After logout → sign-in page —

HTTP status codes worth knowing

401 = who are you? (sign in) · 402 = Payment Required (a rarely used but fitting code) · 403 = never allowed · 302 = go over there instead (a redirect).

Part 6: surprises with the hosting#

FTP suddenly couldn't see the site

Mid-session, the FTP login started showing a different folder, so our usual upload route vanished. The websites were fine; only FTP's view had changed. We switched to the host's file API: one call to download a file (for backups), and a resumable upload for each file. Never the API's "deploy", which replaces a whole site.

403 for Python, 200 for curl

The same API request worked from curl but failed from Python. The difference was the User-Agent header: the host's firewall blocks Python's default one. Giving our helper its own name fixed it.

Every backup was one byte short

The download API drops a file's final newline. The response includes the true size in bytes, so the helper adds the newline back when exactly one byte is missing, and every backup is now verified against the server's size.

Key takeaways#

  • Gate the thing of value: AI calls on the server, page files through a gate, browser code behind the gate.
  • Pages need cookies (sent automatically); API calls can use tokens in headers.
  • Use HttpOnly + Secure + SameSite cookies for sessions.
  • Remove "helpful" fallbacks that bypass security.
  • Test as a stranger, a free user, a paying user and a logged-out user.
  • When a tool disappears (FTP), use the platform's official API, carefully, one file at a time, with verified backups.

Quick quiz#

1. Why couldn't the gate use the token stored in localStorage?

Opening a page doesn't send custom headers, and only JavaScript can read localStorage. Cookies are sent automatically with every request.

2. What does HttpOnly protect against?

JavaScript (including malicious injected scripts) can't read the cookie, so it can't be stolen that way.

3. Meeting Minutes didn't need the gate. Why?

Its valuable part, the AI call, already happens on the server, which checks the login.

Try it yourself#

See the cookie

Sign in on the tools site, open your browser's developer tools → Application/Storage → Cookies. You'll see sgai_session marked HttpOnly and Secure. Then type document.cookie in the console: the session cookie isn't listed, because JavaScript can't see it.