# Browser client

> Repositories in the browser that follow the session.

Source: https://bettersupabase.com/docs/frontend/client

```ts title="src/lib/supabase/client.ts"
import { createClient } from "better-supabase/client";
import { betterSupabase } from "./index";

export const bs = createClient(betterSupabase, {
  env: {
    url: import.meta.env.VITE_SUPABASE_URL,
    publishableKey: import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY,
  },
});

const customers = await bs.db.customers
  .findMany({ select: ["id", "name"] })
  .orThrow();
```

* `bs.db` carries the current session's actor and claims, so plugins
  like [`actor`](/docs/plugins/actor) and [`tenant`](/docs/plugins/tenant)
  behave as they do on the server. It is rebuilt when the user changes.
* `bs.queries` holds [TanStack Query options](/docs/frontend/query).
* `bs.auth` is a small store (`current()`, `subscribe()`) for UI state:
  `loading`, `signed-out`, or `signed-in` with the user and decoded claims.
  The claims are for display; RLS is what enforces access.
* `bs.supabase` is the supabase-js client, for auth flows, storage and
  anything else.

## Storage [#storage]

| `storage`           | Use it for                                                                                    |
| ------------------- | --------------------------------------------------------------------------------------------- |
| `cookies` (default) | apps with a server (Next.js, SvelteKit, …): the session is shared via `@supabase/ssr` cookies |
| `local`             | SPAs without a server: the session lives in localStorage                                      |

With `cookies`, `cookies: { encode: "tokens-only" }` keeps the user object
out of the cookie, and `auth.userStorage` sets where auth-js keeps it instead
(localStorage by default). Use the same `encode` as the server; see
[Encoding](/docs/auth/sessions#encoding).

With `local`, the `cookies` options are ignored and no cookie is written, so
a server (a loader, an API route or the proxy) never sees the session. Send
the access token as an `Authorization: Bearer` header to call your own API;
the server adapters read the bearer token before the cookie. Pick one mode
per app: switching moves the session, so users sign in again once.

For React Native and Expo, use
[`better-supabase/client/native`](/docs/frontend/react-native) with your own
supabase-js client. It has the same `bs.db`, `bs.queries` and `bs.auth`, never
imports `@supabase/ssr`, stores the session in the device keychain with
`secureStorage`, and refreshes it only in the foreground with
`autoRefreshOnForeground`.

The publishable key and URL are validated like on the server: a secret key
or a legacy JWT key is rejected before any request is made.

## Edge Functions [#edge-functions]

`functions<Contracts>(bs.supabase)` calls Edge Functions defined with
`defineFunction`, typed from their own code, and returns a `Result`:

```ts
import { functions } from "better-supabase/client";
import type { CreateCustomer } from "../supabase/functions/create-customer/handler.ts";

const fns = functions<{ "create-customer": CreateCustomer }>(bs.supabase);
const customer = await fns
  .invoke("create-customer", { name: "Acme", organizationId })
  .orThrow();
```

See [Edge Functions](/docs/platform/edge-functions) for the function side and
the errors `invoke` returns.