---
title: Expo App with Hono, tRPC, and Better Auth
description: Create an Expo Router app with a Hono API, tRPC, Better Auth email sign-in, Drizzle, and SQLite in one Turborepo workspace using Better Fullstack.
translationStatus: pending
updated: 2026-10-06
category: React Native
tags:
  - expo
  - react-native
  - hono
  - better-auth
keywords:
  - expo backend starter
  - expo better auth
  - react native trpc hono
  - expo router auth starter
  - mobile app with api starter
---

Use this stack when a native app needs its own backend from the first commit: sign-in, typed API calls, and a database in the same repository.

```npm
npm create better-fullstack@latest my-expo-app -- \
  --ecosystem typescript \
  --frontend native-uniwind \
  --backend hono \
  --runtime bun \
  --database sqlite \
  --orm drizzle \
  --db-setup none \
  --auth better-auth \
  --api trpc \
  --mobile-navigation expo-router \
  --mobile-ui uniwind
```

The command uses `--ecosystem typescript` on purpose. The `react-native` ecosystem generates the mobile app only and does not add a backend, database, or auth; a native frontend inside the TypeScript ecosystem does.

## What this creates

- An Expo app in `apps/native` with Expo Router, a drawer and tab layout, Uniwind styling, and HeroUI Native components.
- Email and password sign-in and sign-up components backed by the Better Auth Expo client, which stores the session with `expo-secure-store`.
- A Hono server in `apps/server` that mounts Better Auth at `/api/auth/*` and tRPC at `/trpc/*`.
- A tRPC router with a public `healthCheck`, a protected `privateData` query, and protected `tasks` procedures.
- Drizzle on SQLite through libSQL, with auth tables and a `task` table.
- A Turborepo workspace that runs the app and the server together.

## Generated shape

```txt
my-expo-app/
├── package.json
├── turbo.json
├── apps/
│   ├── native/
│   │   ├── app/
│   │   │   ├── _layout.tsx
│   │   │   └── (drawer)/
│   │   ├── components/
│   │   │   ├── sign-in.tsx
│   │   │   └── sign-up.tsx
│   │   ├── lib/auth-client.ts
│   │   ├── utils/trpc.ts
│   │   └── .env
│   └── server/
│       ├── src/index.ts
│       └── .env
└── packages/
    ├── api/
    ├── auth/
    ├── config/
    ├── db/
    └── env/
```

## Prerequisites

- Node.js and npm for the workspace, and Bun for the server, which runs with `bun run --hot`.
- Expo tooling, plus an iOS simulator, Android emulator, or physical device.

## Run and verify

```bash
cd my-expo-app
npm install
npm run db:push
npm run dev
```

`db:push` applies the Drizzle schema to the SQLite file named in `apps/server/.env` (`DATABASE_URL=file:../../local.db`). `npm run dev` starts both apps through Turborepo; `npm run dev:server` and `npm run dev:native` start one at a time.

The home screen calls `trpc.healthCheck` and shows a status chip. Without a session it renders the sign-in and sign-up forms. It also queries the protected `privateData` procedure, which returns a message only for a signed-in user.

The root `npm run test` script runs Vitest in the server and API packages with `--passWithNoTests`. No test files are generated.

## How the session reaches the API

On native platforms the tRPC client copies the Better Auth cookie into each request:

```ts
headers() {
  if (Platform.OS === "web") {
    return {};
  }
  const headers = new Map<string, string>();
  const cookies = authClient.getCookie();
  if (cookies) {
    headers.set("Cookie", cookies);
  }
  return Object.fromEntries(headers);
},
```

The server builds the tRPC context from the same headers, and `protectedProcedure` rejects requests without a session:

```ts
export async function createContext({ context }: CreateContextOptions) {
  const session = await auth.api.getSession({
    headers: context.req.raw.headers,
  });
  return { session };
}
```

The `tasks` procedures are ready on the server, but no screen calls them yet. They are a good first feature to wire into a tab.

## Environment and devices

`apps/native/.env` sets `EXPO_PUBLIC_SERVER_URL=http://localhost:3000`. That works in an iOS simulator on the same machine. A physical device or an Android emulator cannot reach the computer's `localhost`, so set the URL to the machine's LAN address and restart Expo.

`apps/server/.env` holds a generated `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `CORS_ORIGIN`, and `DATABASE_URL`. Treat the secret as local-only and generate a new one for each deployed environment.

## Compatibility notes

- Better Auth's server config lists `mybetterfullstackapp://` in `trustedOrigins`, while `apps/native/app.json` sets the app scheme to the project name (`my-expo-app`). Add `my-expo-app://` to `trustedOrigins` in `packages/auth/src/index.ts` so the trusted list includes the app's own scheme.
- `--frontend native-uniwind` selects Expo with Uniwind; `native-bare` and `native-unistyles` are the other Expo variants.

## Deployment notes

Mobile and server ship separately. Build the app with your Expo build profiles, and set `EXPO_PUBLIC_SERVER_URL` to the deployed API at build time. Deploy `apps/server` to a host that runs Bun, and point `DATABASE_URL` at a hosted libSQL or Turso database. Turso also needs an auth token, which this scaffold does not read because the command selects `--db-setup none`. Add `DATABASE_AUTH_TOKEN` to the server schema in `packages/env/src/server.ts`, pass `authToken: env.DATABASE_AUTH_TOKEN` to `createClient` in `packages/db/src/index.ts`, add `authToken: process.env.DATABASE_AUTH_TOKEN` to `dbCredentials` in `packages/db/drizzle.config.ts`, and set the token in the server environment. Do not put server secrets in the mobile bundle; anything prefixed `EXPO_PUBLIC_` ships to devices.

## Troubleshooting

| Symptom                                     | Check                                                                                   |
| ------------------------------------------- | --------------------------------------------------------------------------------------- |
| Status chip shows `OFFLINE` on a device     | `EXPO_PUBLIC_SERVER_URL` must use an address the device can reach, not `localhost`.     |
| Sign-up fails with a missing table error    | Run `npm run db:push` before signing up.                                                |
| `privateData` returns `UNAUTHORIZED`        | Sign in first; the query is a `protectedProcedure`.                                     |
| Auth requests are rejected for their origin | Add the app scheme from `app.json` to `trustedOrigins`.                                 |
| No `apps/server` folder was generated       | Use `--ecosystem typescript`; the `react-native` ecosystem does not generate a backend. |

## Next steps

- Compare with the mobile-only [Expo Uniwind Native App](/guides/react-native/expo-uniwind-native/).
- Read the [React Native ecosystem docs](/docs/ecosystems/#react-native).
- Review the [CLI create reference](/docs/cli/create/).
