GitHat Embed: no-framework drop-in reference
Developer reference for embedding GitHat sign-in with one script tag — covers the script tag, data-* attributes, custom elements, events, programmatic API, app-key setup, and CORS/domain verification.
Overview
The @githat/embed package ships a single IIFE bundle (dist/embed.js) that registers three HTML custom elements and exposes a window.GitHat global. Drop one script tag into any HTML page — Wix, Squarespace, Webflow, Shopify, plain HTML — and GitHat sign-in works without npm, a bundler, or React.
The embed page at /docs/embed currently renders a ComingSoon placeholder. All behaviour documented here is verified directly from the source at /Users/Rensley/Desktop/claude/githat/MicroFrontEnds/packages/embed/src/embed.ts.
The script tag
Include the script tag once, before any GitHat custom elements, usually before </body>. The data-key attribute is mandatory — without it no publishable key is set and CORS will not be permitted for your origin.
<script
src="https://cdn.githat.io/embed.js"
data-key="pk_live_xxxxxxxxxxxxxxxxxxxx"
></script>Script tag attributes
The bundle reads its configuration from its own script element at load time using document.currentScript (with a querySelector('script[src*="githat"][src*="embed"]') fallback for module contexts).
data-key (required): Your publishable key — the pk_live_ token from the GitHat dashboard. This value is public; it is safe to commit to front-end source.
data-api-url (optional): Override the API base URL. Defaults to https://api.githat.io. Useful if you are self-hosting or testing against a staging backend.
data-auth-host (optional): Override the GitHat auth host. Defaults to https://githat.io (derived by stripping 'api.' from data-api-url). This is the origin that opens in the popup and posts messages back.
Custom elements
The bundle registers three custom elements once, idempotently, via customElements.define. They can be placed anywhere in the document after the script tag loads.
<githat-signin> renders a styled button. On click it opens the GitHat-hosted /sign-in page. On successful authentication it dispatches a githat:signin CustomEvent (bubbles: true) on the element and, if the redirect attribute is present, navigates window.location.href to that path.
<githat-signup> is identical in shape but opens /sign-up and dispatches githat:signup.
<githat-userbutton> polls GET /auth/me (with credentials: 'include') when it connects and re-polls whenever githat:signin or githat:signout events fire on document. When signed in it renders an avatar button (img if avatarUrl is present, initial letter otherwise). When signed out it renders a 'Sign in' button that triggers the sign-in flow.
<!-- Sign-in button -->
<githat-signin redirect="/dashboard"></githat-signin>
<!-- Sign-up button -->
<githat-signup redirect="/onboard"></githat-signup>
<!-- User state button (auto-updates on auth) -->
<githat-userbutton></githat-userbutton>Custom element attributes
All attributes apply to <githat-signin> and <githat-signup>. <githat-userbutton> takes no attributes.
redirect: A path to navigate to after successful auth. Must be a same-origin path starting with /. Protocol-relative (//evil.com), absolute off-origin URLs, javascript:, data:, and backslash variants (/\foo) are all rejected by the safeRedirectAttr guard — they result in null (no navigation, event only). When absent, only the CustomEvent fires.
theme: 'light' (default) or 'dark'. Light renders an #F5A623 background with #0a0a0a text. Dark renders #0a0a0a background with #fff text. Inline styles — no host stylesheet required.
label: Button text. Defaults to 'Sign in with GitHat' / 'Sign up with GitHat'. The label is set via textContent (never innerHTML) so HTML injection in the label value is not possible.
Events
All events bubble, so you can listen on document or any ancestor. The githat:signout event is listened to by <githat-userbutton> but is not dispatched by the embed itself — your application code is responsible for firing it when the user signs out (e.g. after calling your own logout endpoint).
document.addEventListener('githat:signin', (e) => {
// e.detail.user: { id, email, name, avatarUrl }
console.log('signed in:', e.detail.user);
});
document.addEventListener('githat:signup', (e) => {
console.log('signed up:', e.detail.user);
});
document.addEventListener('githat:error', (e) => {
console.error('auth error:', e.detail.error);
});
// Fire this from your own logout handler so <githat-userbutton> resets:
document.dispatchEvent(new CustomEvent('githat:signout'));Programmatic API
The bundle exposes window.GitHat after the script executes. Both methods follow the same popup/redirect logic as the custom elements and resolve with { user } on success.
// Returns Promise<{ user }>
const { user } = await window.GitHat.signIn();
const { user } = await window.GitHat.signUp();
// window.GitHat.config contains the resolved { publishableKey, apiUrl, authHost }Popup vs redirect mode
At click time the bundle calls shouldUseRedirect() to decide the auth flow. It returns true (full-page redirect) on: any iOS device (iPhone, iPad, iPadOS 13+ masquerading as Macintosh with maxTouchPoints > 1), any known in-app browser (Instagram, Facebook/Messenger, TikTok, LinkedIn, Twitter, Snapchat, WeChat, Line, KakaoTalk, Pinterest), or any viewport width <= 768 px. All other cases (desktop Chrome, Firefox, Edge, Safari) use a centered 480x640 popup.
If window.open returns null or a window whose .closed is immediately true (popup blocked), the embed shows a toast — 'Popup blocked — opening sign-in in this window in 3s…' — then redirects automatically. The popup path listens for a postMessage from the auth host with { type: 'githat:signin' }; the message is only accepted when e.origin exactly equals config.authHost.
In redirect mode the auth host receives embed=redirect and return_to=<current href> as query parameters. In popup mode it receives embed=1.
Iframe embeds: data-githat-allow-redirect
When the embed runs inside a cross-origin iframe, a same-window redirect navigates only the iframe. To redirect the parent page you must opt in on the parent by adding the data-githat-allow-redirect attribute to any ancestor element (commonly <body>) and including allow="top-navigation" in the iframe tag. Without the opt-in, redirect-mode navigates the iframe itself — auth completes but the user experience is contained within the frame.
<!-- Parent page -->
<body data-githat-allow-redirect>
<iframe
src="https://your-embed-page.com/widget.html"
allow="top-navigation"
></iframe>
</body>App-key setup
Publishable keys (pk_live_) are created in the GitHat dashboard at /dashboard/apps. From the app overview, POST /apps/{appId}/keys with { name, type: 'publishable' } (org-admin role required). The raw key is returned once in the response and is not shown again — copy it immediately.
The key is 48 random bytes encoded as hex (96 characters, 384 bits of entropy), prefixed with pk_live_. Only its SHA-256 hash is stored in DynamoDB. The raw pk_live_ value is also persisted in the keyValue field for publishable keys specifically so the dashboard can display it; secret keys (sk_live_) are hash-only.
The key prefix (first 14 characters, e.g. pk_live_a1b2c3) is stored separately and shown in the dashboard for identification. Secret keys (sk_live_) are for server-to-server calls only — never put them in the script tag or any client-side code.
How app-key auth works (X-GitHat-App-Key)
Every request from the embed to api.githat.io carries the header X-GitHat-App-Key: pk_live_xxx (case-insensitive; the backend reads both X-GitHat-App-Key and x-githat-app-key). The initCors() function in src/lib/cors.js receives this header and uses it to resolve the CORS allow-origin.
The lookup flow is: (1) hash the incoming key with SHA-256, (2) check an in-memory LRU cache keyed by hash, (3) query the KeyHashIndex GSI in DynamoDB if not cached. On a cache miss, getApiKeyByValue() fetches the key record, which contains the app ID and org ID. The handler then fetches the app record to read its registered domain.
The origin hostname from the browser's Origin header is compared against app.domain and app.customDomain. Both the apex form (example.com) and the www subdomain (www.example.com) are accepted — the _hostMatchesApp function strips a leading www. before comparing. If the origin matches, setCorsOrigin(origin) sets Access-Control-Allow-Origin to that exact origin with Access-Control-Allow-Credentials: true.
CORS preflight (OPTIONS) requests do not carry custom headers in some browsers, so the backend also resolves the origin directly from the Origin header against a DomainIndex GSI query, cached in-memory for 60 seconds per container. On a cold hit the cache miss cost is one DynamoDB query.
Domain registration and verification
To permit a new origin, register its domain with the app via POST /apps/{appId}/domain with body { domain: 'example.com' }. The backend validates the domain format, checks uniqueness against CustomDomainIndex, generates a random verification token, and returns DNS instructions.
Add a TXT record to your DNS: Name _githat.<yourdomain>, Value <token>. Then call POST /apps/{appId}/domain/verify. The backend performs a live DNS TXT lookup. On success it requests an ACM certificate. If the domain is in a Route53 hosted zone the CNAME for certificate validation is auto-created; otherwise the CNAME record is returned for you to add manually.
Both the app's primary domain (app.domain) and custom domain (app.customDomain) fields are checked by the CORS resolver. Once the domain is registered and the TXT record is present, the initCors path will permit that origin. Full SSL provisioning via ACM is a separate step that does not block CORS.
Localhost development
When NODE_ENV is not 'production', the CORS resolver skips all validation for origins with hostname localhost or 127.0.0.1 and sets Access-Control-Allow-Origin to that origin directly. No app key or domain registration is required for local development. JWT auth still applies on all authenticated endpoints — the bypass is CORS only.
Session model
The embed never exposes tokens to the host page's JavaScript. Authentication state lives in httpOnly cookies set by the githat.io auth domain. <githat-userbutton> checks session state by calling GET /auth/me with credentials: 'include', which sends those cookies cross-origin. The response contains { user: { id, email, name, avatarUrl, emailVerified, mfaEnabled, passkeys, createdAt }, currentOrg, orgCount }. The host page only sees the public user fields returned by /auth/me.
CDN delivery
The bundle is served from cdn.githat.io (CloudFront distribution E2EULPAKO6JPCU backed by S3 bucket githat-cdn-058264393501, private with OAC). Cache-Control is public, max-age=300, s-maxage=300. The deployment script at packages/embed/deploy.sh builds with esbuild, uploads embed.js and embed.mjs to S3, and creates a CloudFront invalidation for /embed.js and /embed.mjs. This is a dedicated distribution separate from the auth distribution (E2C6JZKDTH26GO).
The build targets ES2017 and outputs two formats: an IIFE bundle (dist/embed.js, loaded via the script tag) and an ES module (dist/embed.mjs, for bundler consumers).