How We Solved Local Subdomain Development with lvh.me

When building GreenRobot Job Search, we needed to test multiple subdomains locally — aicareers.greenrobot.com for AI/ML jobs and remotedevjobs.greenrobot.com for remote developer positions. Each subdomain shares the same codebase but serves filtered content with its own branding. The challenge? Getting Google OAuth and shared sessions to work across subdomains in local development.

The Problem

In production, session sharing across subdomains is straightforward. You set your PHP session cookie domain to .greenrobot.com, and all subdomains — jobsearch, aicareers, remotedevjobs — share the same login session. Simple.

Local development is a different story. Our main site runs on localhost because that’s what’s registered as an authorized redirect URI in Google Cloud Console for OAuth. But subdomains like aicareers.local or aicareers.localhost are on completely different domains — browsers won’t share cookies between localhost and aicareers.local.

We tried several approaches:

  • *.local domains — Can’t share cookies with localhost. Different domain entirely.
  • *.localhost domains — RFC 6761 says these should resolve to 127.0.0.1, and they do. But localhost is on the browser’s Public Suffix List, which means browsers block cookies set with domain=.localhost. Session sharing is impossible.
  • Registering jobsearch.localhost with Google OAuth — Google rejects it. Their redirect URI validation requires a public top-level domain like .com or .org. The .localhost TLD is not accepted.

The Solution: lvh.me

lvh.me is a free domain that resolves to 127.0.0.1 — including all subdomains. No /etc/hosts changes required:

$ ping jobsearch.lvh.me
PING jobsearch.lvh.me (127.0.0.1)

$ ping aicareers.lvh.me
PING aicareers.lvh.me (127.0.0.1)

Because .me is a real public TLD, everything just works:

  1. Google OAuth accepts ithttp://jobsearch.lvh.me/auth/google-callback.php is a valid redirect URI.
  2. Cookie sharing works — Setting domain=.lvh.me on the session cookie lets jobsearch.lvh.me, aicareers.lvh.me, and remotedevjobs.lvh.me share the same PHP session.
  3. No DNS configuration — All *.lvh.me subdomains resolve to 127.0.0.1 out of the box.

Implementation

The PHP side is minimal. In our session configuration:

$currentHost = strtolower($_SERVER['HTTP_HOST'] ?? '');
$hostNoPort = preg_replace('/:\d+$/', '', $currentHost);

$cookieDomain = '';
if (preg_match('/\.greenrobot\.com$/', $hostNoPort)) {
    // Production: share across *.greenrobot.com
    $cookieDomain = '.greenrobot.com';
} elseif (preg_match('/(^|\.)lvh\.me$/', $hostNoPort)) {
    // Local dev: share across *.lvh.me
    $cookieDomain = '.lvh.me';
}

session_set_cookie_params([
    'domain' => $cookieDomain ?: '',
    'path' => '/',
    'httponly' => true,
    'samesite' => 'Lax',
]);

Our subdomain detection code doesn’t need to know about lvh.me specifically — it just checks the hostname prefix:

if (preg_match('/^aicareers\./', $host)) {
    // Serve AI Careers content
} elseif (preg_match('/^remotedevjobs\./', $host)) {
    // Serve Remote Dev Jobs content
}

This matches aicareers.greenrobot.com in production and aicareers.lvh.me in development with the same code.

For Apache, we added the lvh.me subdomains as aliases to our existing vhost:

<VirtualHost *:80>
    ServerName jobsearch.local
    ServerAlias jobsearch.lvh.me aicareers.lvh.me remotedevjobs.lvh.me
    DocumentRoot "/path/to/public_html"
</VirtualHost>

The Login Flow

  1. User visits http://aicareers.lvh.me/
  2. Clicks Login — redirected to http://jobsearch.lvh.me/auth/login.php?return_to=http://aicareers.lvh.me/
  3. Authenticates via Google OAuth (callback registered on jobsearch.lvh.me)
  4. Session cookie set with domain=.lvh.me
  5. Redirected back to http://aicareers.lvh.me/ — already logged in because the session cookie is shared

Takeaway

If you’re building a multi-subdomain app and need to test OAuth and shared sessions locally, skip the localhost/.local headaches and use lvh.me. It’s a zero-configuration solution that plays nicely with Google’s OAuth restrictions and browser cookie policies.