Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Goostr (static)
Your Google account is your nostr identity. No server, no key management.
Pure static site. No build step. No backend. No install required to run.
How it works
- User clicks Sign in with Google. We request three OAuth scopes:
profile,email, anddrive.appdata. - We look in the user's Google Drive App Data folder for a file
goostr-key-v1.json. (App Data is a per-OAuth-client hidden folder in the user's Drive that only this app's client ID can read. WhatsApp uses it for backups; Enpass uses it for vault sync.)- If found → download it. The 32 bytes inside are the nostr private key. User is now signed into their existing nostr identity.
- If missing → generate 32 fresh random bytes (
crypto.getRandomValues), upload to App Data. User now has a brand-new nostr identity.
- We fetch the user's Google name + picture. For a brand-new identity we publish a kind-0 metadata event with them so it has a profile. For an existing one, if no relay returns a kind-0 within ~6s, we publish it then.
- SessionStorage caches the key inside the tab (max 12h) so reloads don't require re-auth. Closing the tab logs out.
Note: the key is stored, not derived. It's random bytes that live in the Drive file. Delete that file (or revoke/delete the OAuth client, which makes App Data unreadable) and the identity is gone for good.
The demo app
Once signed in, app.js shows your name, avatar and npub, a dot per relay (connecting / open / closed / timeout), your last 50 notes (kind 1), and a box to publish a new note.
Relays
relay.js opens every relay in config.js in parallel (4s timeout each) and lets the UI continue as soon as one is open. Relays that connect later are sent every active subscription plus anything published in the last 60s, so nothing is lost to a slow relay.
Why this is good
- Works across devices: sign in on phone, laptop, anything — same Google account → same nostr identity.
- No nsec to lose, no seed phrase to back up. Recovery == "remember your Google password."
- The seed key never touches this device's disk. It lives in Google Drive (encrypted at rest by Google) and in tab-only sessionStorage.
What this is not
- Not as secure as
keystr(passkey-based). Anyone with your Google password + 2FA can read your nostr key. With keystr/passkeys, the user must physically be at the authenticator device. - Not for situations where you want to be invisible to a single big platform — Google obviously knows you own this Drive App Data file.
- Not safe against script injection. While signed in, the raw key sits in
sessionStorage, readable by any script on the page. That includesnostr-tools, loaded fromesm.shvia the import map inindex.htmlwith no integrity check. Pin/self-host it before trusting this with a real identity. - Every sign-in shows Google's consent screen (
prompt: "consent") unlesssignIn({ silent: true })is used.
One-time Google setup (~2 minutes)
- Open https://console.cloud.google.com/apis/credentials.
- Create or select a project.
- Enable the Google Drive API under "Library."
- Under "OAuth consent screen", configure a minimal app (External + Testing is fine for prototyping; add yourself as a Test User).
- Back in "Credentials" → Create Credentials → OAuth client ID → Web application.
- Under Authorized JavaScript origins add:
http://localhost:5174(for local dev)- your deploy URL (e.g.
https://you.github.io)
- Copy the Client ID — looks like
1234567890-abcdef.apps.googleusercontent.com. - Paste it into
public/config.jsas the value ofCLIENT_ID.
Run it
python3 -m http.server -d public 5174
# or
npx serve public -l 5174
Then open http://localhost:5174. Google requires HTTPS for OAuth in production, but localhost is exempt for dev.
Deploy it
Static folder. Drop public/ into any host. GitHub Pages, Netlify, Vercel, Cloudflare Pages, S3 — all fine. Make sure HTTPS is on, and add your deployed URL to the Google Cloud OAuth client's authorized origins.
Layout
public/
index.html — entry point + setup banner
app.js — UI glue
goostr.js — library: signIn(), restoreSession(), clearSession()
drive.js — Google Drive App Data helpers
relay.js — minimal nostr WebSocket pool with replay-on-late-connect
style.css — dark UI
config.js — CLIENT_ID + relay list (edit this)
Library use
import { signIn, restoreSession, clearSession } from "./goostr.js";
// On boot, try restoring first (sessionStorage):
let session = restoreSession();
if (!session) {
session = await signIn({
clientId: "1234.apps.googleusercontent.com",
onStatus: (msg) => console.log(msg),
});
}
const { signer, profile } = session;
// signer.sign({ kind: 1, content: "hello nostr", tags: [] })