@somewhere-tech/auth
v0.4.1
Published
First-party auth for somewhere.tech apps — session client + React hooks + backend handler. Write zero auth code; the bug-prone token layer ships once, correct.
Maintainers
Readme
@somewhere-tech/auth
First-party authentication for somewhere.tech apps. You write zero auth code — the bug-prone session/token layer ships once, correct, instead of being hand-rolled (and re-broken) in every app.
It owns the part that's actually hard: the access token, the background refresh,
the rotation persistence, surviving network blips, and keeping multiple tabs in
sync (a background tab picks up a rotated session off the storage event, and
a request that raced a rotation retries once with the fresh pair instead of
logging the user out). Those are exactly the bugs every hand-written auth layer
hits (logged out on a wifi hiccup or by a second tab, the
rotating-refresh-token desync, the half-written token pair).
Sessions are httpOnly cookies by default (0.2.0)
In a browser, the session now lives in httpOnly cookies set by your
backend — there are no tokens in localStorage (or anywhere page JS can
reach), so an XSS can't exfiltrate the session. The client sends
credentials: 'include', the platform refreshes the cookie pair server-side,
and the only thing cached locally is the (non-secret) user object so a page
load paints the signed-in UI instantly while /me re-validates in the
background. A network blip is never a logout — only an explicit auth
rejection signs the user out.
Nothing to configure; mode is negotiated per sign-in:
- Browser + current backend → cookie session (the default).
- Browser + older backend that doesn't set cookies → automatic fallback to header/token mode; everything keeps working.
- Existing signed-in users upgrading from 0.1.x keep their session (header transport is adopted as-is) and migrate to cookies on their next sign-in.
- CLI / native / Node (no browser cookies) → header/token mode, same
as 0.1.x. Force a mode with
createSomewhereAuth({ mode: 'header' })(or'cookie').
In cookie mode getSession() is always null — the tokens are deliberately
unreadable. Use useUser() / getCachedUser() / getUser() for signed-in
state.
Cookie-authed requests are origin-checked server-side: a cross-origin page (including another project on the platform's shared domain) can't ride the cookies into your API. Browser calls to your backend must be same-origin — which they are, for every normal app shape.
Frontend (React)
import { SomewhereAuthProvider, SignedIn, SignedOut, useUser, useAuth } from '@somewhere-tech/auth/react';
function App() {
return (
<SomewhereAuthProvider>
<SignedIn><Dashboard /></SignedIn>
<SignedOut><SignIn /></SignedOut>
</SomewhereAuthProvider>
);
}
function Dashboard() {
const user = useUser(); // null | User — re-renders on login/logout/rotation
const auth = useAuth(); // actions + auth.fetch
// auth.fetch is a drop-in fetch that carries the session and persists rotation.
// You never touch a token.
const load = () => auth.fetch('/api/notes').then(r => r.json());
return <p>Hi {user?.email}</p>;
}
function SignIn() {
const auth = useAuth();
return <button onClick={async () => {
window.location.href = await auth.googleSignInUrl(); // platform-owned Google — no Google project
}}>Continue with Google</button>;
}Google callback route:
import { AuthCallback } from '@somewhere-tech/auth/react';
// at your redirect_uri route:
export default () => <AuthCallback />; // reads ?code=, completes sign-in, doneBackend (one function)
login/signup are developer-key-gated on the platform (so they can't be
called from the browser). Mount the handler once and it wires them onto
sw.auth:
// functions/api/auth/[...path].ts — handles your /auth/* routes
import { somewhereAuth } from '@somewhere-tech/auth/server';
export default (req, sw) => somewhereAuth(req, sw);That's the whole integration. The frontend client talks to these routes; the
platform's sw.auth does JWT validation + the header-based auto-refresh; the
client persists the rotated pair atomically.
Billing / entitlements (sw.billing)
Define plans + the features each grants in code (no dashboard editor), then gate
on a stable feature slug — not a raw plan string. The user's feature list rides
the session, so has() is a synchronous local check (no network):
// once, from a backend function — declarative, idempotent
await sw.billing.definePlans([
{ slug: 'free', name: 'Free', features: [] },
{ slug: 'pro', name: 'Pro', price_cents: 2000, interval: 'month',
features: ['export', 'api_access'] },
]);import { useEntitlements, Gate } from '@somewhere-tech/auth/react';
function ExportButton() {
const { has } = useEntitlements();
return has('export') ? <Export/> : <Upsell/>;
}
// or declaratively — the entitlement mirror of <Protect>
<Gate feature="export" fallback={<Upsell/>}><Export/></Gate>;Server-side, gate with the same slug: if (await sw.billing.has(user.id, 'export')).
The plan a user is on is kept current automatically by the Stripe checkout
webhook — you write zero webhook code.
Drop-in pricing + manage UI (both ride the session — no extra wiring):
import { PricingTable, BillingPortal } from '@somewhere-tech/auth/react';
<PricingTable/> // catalog plans, current plan highlighted, Subscribe → checkout
<BillingPortal/> // one-click manage / cancel via the Stripe portalLogic-only/unstyled — theme with the sw-* class hooks. Or wire it yourself:
auth.billing.subscribe(planSlug) and auth.billing.openBillingPortal() (both
redirect to Stripe). Subscribe checks out for the signed-in user (resolved
server-side, never a client id) and the price comes from the plan's
stripe_price_id in your catalog. auth.billing.plans() returns the catalog.
Framework-agnostic core
No React? Use the client directly:
import { createSomewhereAuth } from '@somewhere-tech/auth';
const auth = createSomewhereAuth(); // { baseUrl, authPath, storage, storageKey } all optional
await auth.signIn({ email, password });
const res = await auth.fetch('/api/notes'); // session-aware fetch
auth.onChange(({ user }) => { /* re-render */ });Why this exists
The platform already does the hard 80%: the OAuth client (no Google Cloud
project, no provider credentials), JWT validation (sw.auth.fromRequest), and
the refresh protocol. The missing 20% was the thin client layer — and that
thin layer is precisely the bug-prone part. Shipping it once, correct, erases an
entire class of auth bugs.
Status
Covered: email/password, Google (platform-owned), httpOnly cookie sessions
(browser default) with dual-mode fallback, session-aware fetch with atomic
rotation + blip resilience, optimistic cached user, React
hooks/gates/callback. Magic-link for end users is a follow-on. 0.x — API
may still move.
