Cloud|

Sign-in locations

Manage bundled and uploaded sign-in location databases, updates, proxy requirements, and privacy limits.

11 min read Updated 2026-10-19 #operations#identity#privacy

Core includes a server module and a bundled database snapshot that describe a sign-in request by browser, operating system, approximate place, and network operator. Successful browser sign-ins use this module to remember browsers and countries. Paired apps can request browser sign-in context through the signed app approval API. Cloud Login displays it when the Cloud announces support; see Read the request details. Administrators can inspect and replace the databases on the admin page, through the admin API, and with the CLI.

Bundled snapshot and updates

Only the Core image contains the DB-IP IP to City Lite and IP to ASN Lite snapshot for 2026-10, built on 2026-10-01. The files live under /app/geoip: dbip-city-lite.mmdb (127 MB) and dbip-asn-lite.mmdb (9.6 MB) uncompressed. The image build takes the gzip files (about 65 MB compressed) from Cloud's image mirror and checks their checksums.

Upgrade Cloud to update the bundled snapshot: each release carries the snapshot pinned at build time. You can also replace either database with your own upload until you reset it. There is no GeoIP environment variable and no automatic download at runtime.

Without a bundled file or an upload for a type, as in development, tests, or custom images, the module returns no place or network name for that type. Browser and operating system descriptions still work. When first used, it logs each missing or invalid database once; sign-ins load the module when they describe the request. A lookup failure also returns no details from the affected database and logs once. Core maps the files through Bun, which needs write access to open them, although Core never writes to them. With a read-only root filesystem, such as readOnlyRootFilesystem or docker run --read-only, bundled lookups therefore return no place or network details and log each database's failure once. Uploaded databases do not need filesystem write access.

The databases are © DB-IP.com, licensed under CC BY 4.0: IP Geolocation by DB-IP. They are decompressed from gzip and otherwise unmodified. The admin page shows this attribution while any active database is from DB-IP. Any page that displays or uses DB-IP database results must link to DB-IP.com.

Use your own database

Open Admin → Settings → Sign-in → Sign-in locations to inspect the source and build date of each database, upload a replacement, or reset it to the bundled snapshot. The page shows a hint when active data is older than 183 days, about six months. The build date comes from the file, not the upload date.

Upload a City or ASN database in MaxMind DB format with an IPv6 search tree. DB-IP Lite and MaxMind GeoLite2 databases work. Follow the database provider's licence; GeoLite2's EULA requires regular updates. Files can be raw .mmdb or .mmdb.gz; Cloud detects gzip from the file contents.

The limits are 192 MiB unpacked and 128 MiB per request body. A reverse proxy may set a lower request limit: upload the .gz file in that case. The CLI compresses raw files while sending them:

bash
cld admin geoip status --json
cld admin geoip upload ./dbip-city-lite.mmdb.gz
cld admin geoip reset city --yes

Use reset asn --yes to reset only ASN, or reset --yes to reset both types. Both the page and CLI use the same administrator API. An OAuth token also needs the read scope to show the status and write to upload or reset; admin allows both. One upload runs at a time per Core process; if another is running, wait briefly and retry. Invalid or oversized files leave the previous database active. A record that needs more than 32 KiB to decode gives no place or network name, and Core logs that once. The upload check samples about a thousand addresses and refuses the file when one of them reaches such a record. DB-IP Lite records need less than 1 KiB. Cancelling an upload in the page stops it before it is saved.

Uploads are stored in Postgres (geoip.databases), so database backups include them. Each type has one current upload, which replaces the bundled file until reset. Every Core instance checks for changes on lookups about once a minute and switches to the replacement. A restarted Core also starts a refresh without delaying startup. Uploads and resets have audit entries with the type, build date, checksum, size, and administrator; file contents are not included in the audit.

An uploaded database is held in each Core process's memory, using about its unpacked file size. The bundled snapshot is memory-mapped. A type uses one reader at a time. Plan memory for the upload and for other instances reloading it; uploads do not change the server's request-body limit.

Privacy and accuracy

The address stays on the server; lookup is local. The module stores and logs no IP address. Its result includes only browser and system codes, country, city, and network operator, plus database availability.

Locations are approximate. A city can identify a provider's network node. VPNs and mobile networks can point to a different city or country from the person using them. Network operator names do not classify a connection as a VPN or data center.

Configure trusted reverse proxies so the gateway forwards the person's address. Otherwise results can describe the proxy. Private and other non-public addresses produce no place or network name.

A successful browser sign-in sets __Host-cloud_browser. It is HTTP-only, Secure, SameSite=Strict, and has Path=/ with no Domain attribute. In HTTP development, the name is cloud_browser and Secure is omitted.

The value is 32 random bytes encoded as 43 base64url characters without padding. The cookie lasts 90 days. Each successful sign-in refreshes its lifetime and keeps the same value. A missing or invalid value gets a new one. Core does not rotate it. Signing out keeps it. Failed sign-ins do not set or refresh it. The mobile app does not get this cookie.

Stored data

Core stores browser and country history in three Postgres tables:

  • auth.device_trust_state has one installation-wide row (id). started_at records the first migration start and never changes. geoip_since records when Core first stored a country and is set only once.
  • auth.known_browsers has a row ID, account ID, browser hash, browser and operating system codes, and the latest sign-in's country and city. It records the first and latest sign-in times. notified_at records the notice handoff time, or the first-seen time for browsers skipped during introduction or remembered before this notice existed. notice holds the fixed sign-in snapshot until handoff, then becomes NULL. It is limited to 1,024 bytes of PostgreSQL JSONB text. notice_attempted_at records a failed handoff's last attempt. Cities are limited to 96 UTF-8 bytes.
  • auth.sign_in_countries has an account ID, country code, and latest sign-in time for each account and country pair.

Only the hash sha256(user ID ‖ cookie value) is stored, as 32 raw bytes. The input is the lowercase UUID text followed directly by the cookie text. The cookie value itself is not stored in Postgres. A database dump alone cannot link two accounts in one browser. Anyone who sees the cookie value at runtime can compute the hash for any account. This includes an operator who can see cookies in proxy logs or a browser.

Core also stores the request's context snapshot in auth.app_logins.context. It contains the level, browser/OS codes, country, city, operator, and history. It is limited to 512 bytes of PostgreSQL JSONB text, including spaces and escaped characters. Core first removes city and network names if needed, then drops the whole context if it still does not fit. A context failure never fails the sign-in. Decoy and terminal rows have context = NULL. The snapshot lives with the request until expiry and bounded cleanup. It never contains sameNetwork or an address.

No raw IP address is stored in these tables. No legal review was done for this storage or cookie behavior.

Same-network hint

After a browser request commits, Core stores a request address pseudonym in Valkey: SHA-256 of a fresh 16-byte salt followed by the full IPv4 address or IPv6 /64. The salt is stored beside the digest and differs for each request. The value has no account data or raw address, and the key is per request. Decoys do the same work. Private or unknown addresses produce no entry.

The key expires with the five-minute request and is deleted after approval or denial. A signed context read compares the phone's prefix with this pseudonym and returns sameNetwork: true only on a match. Core reads the listed browser requests in one Valkey call after the SQL transaction. If Valkey is down, the hint is missing and sign-in continues. Nothing about S5 changes the level. This is pseudonymization, not anonymization: someone with the salt and digest can test candidate addresses.

Retention and limits

Browser and country records expire 90 days after last_seen_at. Every read applies this cutoff, even before a cleanup run deletes the row. A later sign-in from an expired browser starts a new first-seen period.

Core's per-minute maintenance job deletes expired records in batches of up to 1,000 rows, with at most 10 batches per table per run. The aim is deletion within a day after expiry. A stopped job only delays deletion; it never extends how long a record counts as remembered.

Each account keeps at most 100 browsers and 50 countries. New sign-ins evict the oldest records first. An installation-wide introduction phase lasts 14 days from the first Core start after the upgrade. During it, context has level: "info" and history: null; Cloud makes no known/new claim because Core has not seen the browsers yet. Accounts and requests cannot restart or extend it.

New-browser notice

After a successful web sign-in from a new browser, Core hands over New sign-in from a new browser after the session transaction commits and before the response returns to the browser. This includes password, passkey, email-code, Cloud Login, password-reset, expired-password, and emergency-admin sign-ins. Mobile app session issuance is excluded. An email-code sign-in that creates a guest account sends no notice, because the account did not exist before.

The browser notice shows browser, system, and time, and opens My account → Sign-in (/me/security). The email also shows the sign-in method and, when available, the approximate place. Cloud Login includes the approving device's name. The time uses the installation's app.timezone setting, default UTC, and names that zone. The notice uses the sign-in request's locale; if no locale was stored, it uses the installation's app.locale.

The notice is on by default, with email first and a browser notification when there is no email address. The person can turn it off under My account → Notifications, entry New sign-in from a new browser. Preferences are read at handoff, when delivery plans are persisted. They are not captured with the sign-in snapshot. The handoff precedes the response so the person signing in cannot turn the notice off first. A notice that maintenance catches up after a failed handoff uses the preferences at that later time.

A failed handoff does not fail the sign-in. The fixed snapshot stays in notice, with notified_at = NULL. Later sign-ins can update the row's latest place, but do not change a pending snapshot. Core's existing per-minute maintenance job retries one batch of at most 100 notices: never-retried notices first, then the least recently tried, oldest first within each. A failure leaves that notice pending, records the attempt time in notice_attempted_at, and moves it behind the others, so failing notices cannot hold back the rest. Malformed snapshots are discarded without logging their content.

The idempotency key is new-browser-sign-in:<browserId>:<signedInAt>. Retries of the same first-seen period produce one event. A browser returning after the 90-day retention window counts as new again, even when its row ID survives; its new first-seen time gives it a different key.

During the installation-wide 14-day introduction phase, browsers are marked announced without sending a notice. Maintenance never sends those notices later. On the first migration that adds notice, previously remembered browsers also count as announced. Rerunning that migration leaves pending notices untouched.

Where traces of a sign-in end up

Storage What it contains Retention and deletion
auth.device_trust_state Installation start and first stored country time Installation-wide; neither timestamp is reset by sign-ins or account deletion.
auth.known_browsers Account, browser hash, browser, OS, latest country and city, first and latest sign-in, notification time, and a pending snapshot of at most 1,024 bytes 90 days after the latest sign-in; at most 100 per account; snapshot cleared at handoff; account deletion removes rows by CASCADE.
auth.sign_in_countries Account, country and latest sign-in 90 days after the latest sign-in; at most 50 per account; account deletion removes rows by CASCADE.
auth.app_logins.context Request context, at most 512 bytes of JSONB text; no S5 or address; empty for terminal and decoy requests Five-minute request lifetime, then existing bounded expiry cleanup; account deletion removes account rows by CASCADE.
App-login address key in Valkey Per-request salt and SHA-256 digest of full IPv4 or IPv6 /64; no account data Expires with the request; deleted after approve or deny; missing when Valkey is unavailable.
audit.events for app-login approve/deny Browser decisions include the stored level only, alongside the request ID; no city, country, network, browser/OS, or S5 Existing audit retention; terminal decision metadata stays unchanged.
Notification history (notifications.events) Notice title and target, without place Existing notification history retention; account deletion removes events by CASCADE.
Notification delivery (notifications.deliveries) Encrypted delivery content, including place in the email payload A database trigger clears the payload when delivery is delivered, suppressed, or failed.
Outgoing mail Email content, including approximate place outgoing_mail.content_retention_days, default 90 days; independent of account deletion.
Existing rate limiter in Valkey IP addresses used briefly for rate limits Unchanged; short-lived rate-limit keys.
auth.session_families.created_user_agent The sign-in's user-agent header Unchanged; follows session-family retention.
Proxy logs and backups Depends on the operator's configuration Outside Cloud's retention and deletion jobs.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.