signup-geo.ts53 lines · main
1import { newId } from '@briven/shared';
2
3import { getDb } from '../db/client.js';
4import { authSignupGeo } from '../db/schema.js';
5import { lookupIp } from '../lib/geoip.js';
6import { log } from '../lib/logger.js';
7
8/**
9 * Platform-wide sign-up geo capture (admin-only SEO analytics).
10 *
11 * Called once per end-user sign-up from the per-tenant Better Auth
12 * user.create hook. Does a self-hosted geo lookup (lib/geoip.ts, GeoLite2
13 * .mmdb when BRIVEN_GEOIP_DB_PATH is set, HTTP fallback otherwise) and
14 * inserts ONE row into the control-plane auth_signup_geo table.
15 *
16 * HARD rule: this must NEVER break sign-up. Every failure path (missing geo
17 * DB, DB insert error, unparseable IP) is caught and logged at warn — the
18 * caller does not await it in a way that blocks the auth response, and even
19 * if it did, a throw here can't escape the try/catch below.
20 */
21export interface CaptureSignupGeoInput {
22 projectId: string;
23 userId: string;
24 email: string | null;
25 ip: string | null;
26}
27
28export async function captureSignupGeo(input: CaptureSignupGeoInput): Promise<void> {
29 try {
30 // lib/geoip.ts already handles: no-ip → null, private/localhost → null,
31 // missing .mmdb → single warn + HTTP fallback, all fields undefined-safe.
32 const geo = await lookupIp(input.ip);
33
34 await getDb()
35 .insert(authSignupGeo)
36 .values({
37 id: newId('me'),
38 projectId: input.projectId,
39 userId: input.userId,
40 email: input.email,
41 ip: input.ip,
42 country: geo?.country ?? null,
43 city: geo?.city ?? null,
44 region: geo?.region ?? null,
45 });
46 } catch (err) {
47 // A geo/analytics miss must never surface to the signing-up user.
48 log.warn('signup_geo_capture_failed', {
49 projectId: input.projectId,
50 message: err instanceof Error ? err.message : String(err),
51 });
52 }
53}