Ulabase

Tools

The Ulabase Kit

The client side of your backend: sign-up, sign-in, sessions, teams, invitations and payments for React, Angular, Vue, Next.js and Nuxt. One package.

The Ulabase Kit is the client code that talks to your service: sign-up, sign-in, email verification, password reset, teams and invitations, and, for an app that sells something, subscriptions and orders.

npm install @ulabase/kit
import { checkSession, login, logout } from '@ulabase/kit';

const config = { apiBaseUrl: 'https://c0ffee.ulabase.app' };

const user = await checkSession(config);   // UserInfo, or null
await login(config, 'user@example.com', 'secret');
await logout(config);

Every function takes the same config as its first argument. There is nothing to initialise or register, and no state in a module global.

What it saves you

Sign-up, sessions and checkout each take several steps.

Signing up is a POST, an email, a verification link that lands on a different origin, a redirect carrying a token in a URL fragment, and a token to store. A session is a JWT that expires in fifteen minutes and has to be renewed before it does, from a timer that survives a backgrounded tab. A checkout is a redirect to Stripe, a return to a page that knows which order came back, and a webhook that may arrive after the customer does.

The kit handles those flows against your service’s sign-up and Stripe features. It has no dependencies of its own and works with any framework.

The packages

Package What it adds

@ulabase/kit

The core. Plain TypeScript, zero dependencies, works with any framework or none. Everything below is built on it.

@ulabase/kit-ng

Angular: provideRhAuth(), RhAuthService and RhPaymentsService on signals, route guards, and an HTTP interceptor that attaches the token and clears the session on 401.

@ulabase/kit-react

React: RhAuthProvider, useAuth(), usePayments(), route guards. A /next subpath adds Next.js: middleware refresh, a first-party session cookie, the fragment→cookie bridge, and server actions.

@ulabase/kit-vue

Vue: createRhAuth(), useAuth(), usePayments(), navigation guards. A /nuxt subpath does for Nuxt what /next does for Next.

Each adapter wraps the core: the same functions, exposed the way that framework expects state to arrive. Payments have their own API, apart from auth: a subscription loads on sign-in, reloads when the user switches team, and stays untouched when the service has no stripe feature.

Each adapter installs the core as a dependency. Each package’s README has the exact line.

Setting it up

Angular, in app.config.ts:

providers: [provideRhAuth({ apiBaseUrl: environment.apiUrl })]

React, near the root:

<RhAuthProvider config={{ apiBaseUrl: import.meta.env.VITE_API_URL }}>
  <App />
</RhAuthProvider>

Vue, in main.ts:

const rhAuth = createRhAuth({ apiBaseUrl: import.meta.env.VITE_API_URL });
app.use(rhAuth);
router.beforeEach(rhAuth.authGuard);

In all three the session is restored once at start-up, before the first guard runs, so a page reload keeps a signed-in user where they were. Until then initializing is true: show a spinner while it is.

Sessions

The token is a JWT that lasts fifteen minutes. The kit renews it at 80% of its life, so a tab left open stays signed in. If it expires anyway (a sleeping laptop, a tab in the background for an hour), the next call gets a 401 and the session is cleared: the user sees that they are signed out.

Warning

The kit supports two modes. For a Ulabase service, use bearer.

Bearer (the default) keeps the token in localStorage and sends Authorization: Bearer <token>. It works cross-origin.

Cookie mode has the server manage an HttpOnly JWT cookie, and needs the app and the service to share an origin. Your service lives on .ulabase.app and your app does not, so that cookie is *third-party on every request the page makes. Safari and Firefox block it by default, and in Chrome it is the user’s choice. No CORS configuration changes this: the browser drops the cookie before CORS is consulted.

Stay on 'bearer' unless the app is served from the service’s own origin.

Next.js and Nuxt can use a cookie: a first-party one, set by your own server and holding the same bearer token. It needs no cookie support from the service. The /next and /nuxt subpaths do this for you.

What it covers

Accounts: register, verify, login, logout, checkSession, getUserInfo, renewToken, forgotPassword, resetPassword, changePassword, updateProfile. The flows of Sign-up and login, with the redirects and token deliveries already handled.

Teams: getTeams, switchTeam, createTeam, listTeamMembers, updateMemberRole, removeMember, and the invitation half: invite, getInvitation, activate, acceptInvite, resendInvite. Switching team issues a new token, since the team is a claim in the token.

Consents: acceptConsents, for accepting terms and privacy policy. Use it with Guards, which make acceptance mandatory on the server, where the client cannot skip it.

Payments: getPlans, createCheckoutSession, openBillingPortal, getSubscription, waitForSubscription, and seat licences with getLicenses / grantLicense / revokeLicense. For a shop with one-off orders: getCatalog, createOrder, getOrder, waitForOrder, readOrderRef. You need no Stripe.js and no publishable key: every Stripe page is hosted, and the kit gives you its URL to navigate to. See Stripe billing for the service side.

The customer’s browser usually comes back to your site before Stripe’s webhook does. waitForOrder waits for the webhook, and when it is late it rejects with a WaitTimeoutError, so you can show "still confirming" for a payment that has not failed.

Cart: lines, quantities, subtotal and persistence, ready for any shop. The logic is pure and takes the lines it works on, and each framework wraps it: useCart() in React and Vue, RhCartService in Angular.

const cart = useCart();

cart.add({ productId: 'tee-classic/yellow-l', name: 'Classic T-shirt',
           unitAmount: 2500, options: { colour: 'yellow', size: 'L' } });

await createOrder(config, cart.orderItems);

orderItems is the cart in the shape createOrder takes. Names, prices and pictures stay out: the service reads them from its own catalog. The chosen options go with the order, as the line’s metadata. A reference like tee-classic/yellow-l says which row of the catalog was bought; the options give the seller "yellow, L" in separate fields.

The cart works without a session: it needs no AuthConfig and no auth provider, so visitors can fill a basket before they sign in. It becomes an order at createOrder, which needs a config. It lives in localStorage, so a reload keeps it; pass a storageKey when two of your apps share an origin.

Errors are thrown as an ApiError, { status, message }, with the server’s own message in it.

A note on the users collection

register sends whatever you give it. If the service’s users collection has a JSON Schema, extra fields are validated against it; if it has none, the server silently drops them and still answers 201. If a profile field never appears, this is the usual cause.

  • The ulabase CLI: configure the service the kit talks to from a file in git

  • Tokens: what each token reaches

  • Stripe billing: the stripe feature, its keys, and the permissions a guest checkout needs

  • Guards: server-side rules the client cannot skip

  • Full-stack example: a complete app built on the kit

  • ulabase/kit: the source; each package’s README has the full API