Sign in with a planet.

An interactive globe that detects a visitor's timezone, language and location — so your app greets them right from the first second.

$ npm i @planetlogin/planetlogin
↻ drag the globe or search a place

Why PlanetLogin

A locale picker that feels like magic.

One pick on a real 3D globe and you know who just arrived — no IP lookups, no consent banners, no third-party SDK.

A real globe

Orthographic projection via d3-geo — proper hemisphere clipping, drag with inertia, wheel zoom, click a country.

TZ · language · place

Returns IANA timezone, BCP-47 language and ISO country from a single pick. ~150 countries mapped to a language.

No key, no tracking

Keyless geocoding (Open-Meteo + OSM Nominatim). No analytics, no cookies beyond your own. AGPL-3.0.

Drops in anywhere

Ships as a standard Web Component, a class and a factory. React, Vue, Svelte, Angular or plain HTML. ~17 kB gzip.

Accessible

Keyboard-controllable globe (arrows, +/-, Enter), focus ring, ARIA labels, and respects prefers-reduced-motion.

TypeScript-first

Typed API, ESM + UMD builds, and a tiny readable codebase you can fork and own.

Quickstart

Two lines to a localized welcome.

Listen for one locale event and wire it into your i18n and date handling.

import '@planetlogin/planetlogin';

<!-- anywhere in your HTML -->
<planet-login accent="#f6a13c"></planet-login>

const el = document.querySelector('planet-login');
el.addEventListener('locale', (e) => {
  const { language, timezone, country } = e.detail;
  i18n.setLanguage(language);
});
import { useEffect, useRef } from 'react';
import { createPlanetLogin } from '@planetlogin/planetlogin';

function GlobeLogin({ onLocale }) {
  const ref = useRef(null);
  useEffect(() => {
    const g = createPlanetLogin(ref.current, { onLocale });
    return () => g.destroy();
  }, []);
  return <div ref={ref} style={{ height: 440 }} />;
}
import { onMounted, onBeforeUnmount, ref } from 'vue';
import { createPlanetLogin } from '@planetlogin/planetlogin';

const el = ref();
let g;
onMounted(() => { g = createPlanetLogin(el.value, { onLocale: l => emit('locale', l) }); });
onBeforeUnmount(() => g?.destroy());
import { onDestroy } from 'svelte';
import { createPlanetLogin } from '@planetlogin/planetlogin';

let el, g;
$: if (el && !g) g = createPlanetLogin(el, { onLocale });
onDestroy(() => g?.destroy());

The payload

Everything you need, on one event.

Delivered three ways: the locale DOM event, the on('locale', …) listener, or the onLocale option.

interface PlanetLocale {
  lat: number; lon: number;
  country: string;        // ISO 3166-1 alpha-2
  timezone: string;       // "Europe/Madrid" or "UTC±N"
  language: string;       // "es"
  label: string;          // "Barcelona, Spain"
}

More than a globe

A full auth service — stateless, no database of its own.

The globe is the face. Behind it, @planetlogin/core is a framework-agnostic login service: password, OAuth/OIDC, magic links, passkeys, TOTP and anonymous sessions. It signs an asymmetric JWT (EdDSA) and publishes a JWKS. PlanetLogin keeps no database of its own — it reads and writes your store over REST (or in-process), so your app stays the source of truth. Need zero backend? Run anonymous guest sessions with no store at all. The stores below are optional batteries.

// A stateless login — no session table, no DB inside PlanetLogin.
import { passwordLogin, verifyPassword, signSession } from '@planetlogin/core';

const result = await passwordLogin(
  { downstream, verifyPassword, signSession },
  { identifier: email, password },
);
if (result.ok) setCookie('session', result.token); // signed JWT (EdDSA)
// Your app verifies the token offline — no shared secret, just the JWKS.
import { createRemoteJWKSet, jwtVerify } from 'jose';

const jwks = createRemoteJWKSet(new URL(
  'https://auth.acme.com/auth/.well-known/jwks.json'));

const { payload } = await jwtVerify(token, jwks);
const userId = payload.sub; // authenticated, anywhere, offline
// Batteries-included persistence — accounts with zero downstream code.
import { postgresStore } from '@planetlogin/store-postgres'; // or store-sqlite

const downstream = postgresStore(pool);
await downstream.ensureSchema();
await downstream.createUser({ email, password });
// That's the whole identity layer — passwordLogin() now just works.

Ship it as a flavor (a ready portal for your framework — SvelteKit today, more coming) or embed the core directly. Read the whitepaper →

Open source

Free, copyleft, community-built.

AGPL-3.0 with a small attribution term. Free for any use, including commercial. Issues and PRs welcome.