@proteles/solid
v0.1.1
Published
Solid 1.x primitives and components for Proteles authentication, plus SolidStart server handlers. Tokens never reach the browser.
Maintainers
Readme
@proteles/solid
Solid primitives and components for Proteles authentication, plus the SolidStart server handlers.
Tokens live only in an encrypted, httpOnly cookie set by your own server — the
browser never receives one, only the sanitized user from /api/auth/me.
Install
npm install @proteles/solidThe five-minute setup (SolidStart)
1. The auth routes — the whole OAuth flow, in one file:
// src/routes/api/auth/[...proteles].ts
import { protelesAuthHandler } from "@proteles/solid/server";
export const GET = protelesAuthHandler;
export const POST = protelesAuthHandler;2. Middleware — put the user on the event and guard routes:
// src/middleware.ts (register it via `middleware` in app.config.ts)
import { createMiddleware } from "@solidjs/start/middleware";
import { attachUser, guardRequest } from "@proteles/solid/server";
export default createMiddleware({
onRequest: [
async (event) => {
await attachUser(event);
return guardRequest(event, { publicPaths: ["/"] });
},
],
});3. Wrap the app so the UI can read auth state:
// src/app.tsx
import { AuthProvider } from "@proteles/solid";
export default function App() {
return (
<AuthProvider>
<Router>{/* … */}</Router>
</AuthProvider>
);
}Then use the components anywhere:
import { SignedIn, SignedOut, SignInButton, UserButton, useUser } from "@proteles/solid";
export default function Home() {
const { user } = useUser();
return (
<>
<SignedOut>
<SignInButton />
</SignedOut>
<SignedIn>
Hi {user()?.email} <UserButton />
</SignedIn>
</>
);
}Client API
<AuthProvider>
Wrap your app once. Creates the auth object, provides it to every descendant, and
revalidates against /api/auth/me on mount (so a stale initialUser — e.g.
after a logout in another tab — self-corrects). onMount never runs during SSR,
so server rendering stays pure.
| Prop | Notes |
| --- | --- |
| initialUser | From the server. Omit = fetch on mount, null = known signed-out, an object = known signed-in. |
| basePath | BFF mount path. Defaults to VITE_PROTELES_BASE_PATH or /api/auth. |
useUser() / useAuth()
State is exposed as Solid accessors — call them:
const { user, isLoading, isAuthenticated, signIn, signOut, reload } = useAuth();
return (
<Show when={!isLoading()} fallback={<p>Loading…</p>}>
<Show when={isAuthenticated()}>{user()!.sub}</Show>
<button onClick={() => signIn({ connection: "google" })}>Google</button>
</Show>
);They track inside JSX, createMemo, and createEffect like any signal.
useUser() returns just the three state accessors. Both throw a clear error if
used outside an <AuthProvider>.
isAuthenticated is a plain derived accessor rather than a createMemo, so it
works even when createAuth() is called outside a component (a memo needs a
reactive owner and would silently stop updating there).
Components
| Component | Renders |
| --- | --- |
| <SignedIn> | its children when a user is signed in |
| <SignedOut> | its children when no user is signed in |
| <Protect fallback={…}> | its children when signed in, else fallback (nothing while loading) |
| <SignInButton returnTo? connection?> | a button that starts login (connection picks a social provider) |
| <SignOutButton returnTo?> | a button that logs out |
| <UserButton> | the user's name/email plus a sign-out control |
<SignInButton>/<SignOutButton>/<UserButton> are unstyled by design — pass
class. The three wrappers (<SignedIn>, <SignedOut>, <Protect>) render
only their children and take no class; passing one is a type error.
<SignedIn>/<SignedOut> render nothing while the first /api/auth/me is in
flight, so there's no signed-in/out flash.
Server API (@proteles/solid/server)
| Export | Purpose |
| --- | --- |
| protelesAuthHandler(event, config?) | Handles login/callback/logout/me; assign straight to GET/POST |
| attachUser(event, config?) | Resolves the user and stores it on the event (locals, else context) |
| getUser(event, config?) | The sanitized user (no tokens) — reuses attachUser's value when present |
| getSession(event, config?) | The full session including tokens — server-side only |
| guardRequest(event, { publicPaths }, config?) | A redirect Response when the request should bounce to login, else undefined |
| toWebRequest(source) | The adapter (shared with @proteles/vue): accepts a Web Request, a SolidStart FetchEvent, or an h3 event |
publicPaths entries match exactly or as a path-segment prefix: "/blog" covers
/blog and /blog/post but not /blogging. "/" is exact-only, so listing
the home page doesn't accidentally make the whole app public.
Configuration (environment)
Server-side, read by the handlers — same names as every Proteles BFF package:
| Variable | Required | Default |
| --- | --- | --- |
| PROTELES_ISSUER | ✅ | |
| PROTELES_CLIENT_ID | ✅ | |
| PROTELES_CLIENT_SECRET | | (omit for a public PKCE client) |
| PROTELES_SESSION_SECRET | ✅ | base64 of 32 random bytes |
| PROTELES_REDIRECT_URI | ✅* | * or derived from PROTELES_APP_URL |
| PROTELES_APP_URL | | |
| PROTELES_SCOPES | | openid profile email offline_access |
| PROTELES_COOKIE_SECURE | | true |
| PROTELES_BASE_PATH | | /api/auth |
Client-side, only VITE_PROTELES_BASE_PATH is read (and only if you moved the
routes). Generate a session secret:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Packaging note
Solid's JSX is compiled by its own Babel preset, and it compiles differently for the browser (fine-grained DOM updates) than for SSR (string concatenation). So this package ships the conventional Solid layout:
dist/source/— untranspiled JSX, selected by thesolidexport condition. This is the preferred path: your build (vite-plugin-solid) compiles it for whichever target you're building.dist/dom/anddist/ssr/— precompiled fallbacks for tooling that ignores thesolidcondition.dist/api/— the./serverentry (plain TS, no JSX).
Develop
npm install # from the sdk/ workspace root
npm run build # babel (source/dom/ssr) + tsc types -> dist
npm test # tsx + node:testTests cover the reactive auth object, the components (compiled with
babel-preset-solid and server-rendered via solid-js/web — no bundler, no
jsdom), and the SolidStart bindings for both event shapes. The full loop also
runs against a live server via
sdk/scripts/verify-solid.mjs (Test 14d of
scripts/e2e-smoke-test.sh), which renders the published SSR bundle.
