Standalone mobile starter
What you'll have afterwards:
- A web app on
http://localhost:3000with sign-up, sign-in, account pages and an admin area, signed in with theadminrole. - A mobile app on
http://localhost:3001, and on iOS and Android, that signs in at that web app. - An API route,
/api/me, that the mobile app calls with its access token.
Use this starter when your app needs its own users and API, with nothing else to run. It is the workspace starter plus the app of the mobile starter, wired together:
apps/webhas sign-up and sign-in,/accountpages and an admin area with user management. With OAuth Server it is also the issuer and the API of the mobile app.apps/mobileis a Capacitor app on Mobile. It signs in atapps/webwith Auth in client mode, and picks its guards with Authorization. It calls/api/mewith its access token.
apps/web reads the app's client id and redirect URIs from
apps/mobile/identity.ts.Prerequisites
The tools in Requirements, plus:
| Tool | For |
|---|---|
| Xcode | iOS: a recent Xcode with an iOS simulator. The native project uses Swift Package Manager, no CocoaPods. |
| Android Studio | Android: a Java Development Kit (JDK) and the Android SDK. |
1. Create the project
Copy .starters/mobile-standalone
with pnpm create frigear-app (CLI) or with giget.
pnpm create frigear-app also runs pnpm install and pnpm prepare:env:
pnpm create frigear-app my-app --platform web,mobile
cd my-app
git init
pnpx giget@latest gh:Frigear-nu/frigear-nu/.starters/mobile-standalone my-app
cd my-app
git init
corepack enable
pnpm install
pnpm prepare:env
pnpm prepare:env creates apps/web/.env and apps/mobile/.env from their examples, and generates the secrets. It
never overwrites a value, so it is safe to run again.
| Variable | Where | What it is |
|---|---|---|
NUXT_APP_SECRET | apps/web/.env | Seals the session cookie (Nuxt's appSecret). Generated; 32+ characters in production. |
NUXT_OAUTH_SERVER_PRIVATE_KEY | apps/web/.env | Signs the OAuth tokens. A PKCS#8 RSA key, generated. |
NUXT_ACL_ADMINS | apps/web/.env | Comma separated emails that always get the admin role once verified. Put your own email here. |
NUXT_PUBLIC_AUTHENTICATION_CLIENT_ISSUER | apps/mobile/.env | http://localhost:3000, the web app. |
NUXT_PUBLIC_AUTHENTICATION_CLIENT_API_BASE | apps/mobile/.env | http://localhost:3000, the web app's API. |
2. Run both apps
pnpm dev
This starts apps/web on http://localhost:3000 and apps/mobile on http://localhost:3001. On the first start,
NuxtHub creates a local SQLite database in apps/web/.data/. It applies the committed migrations, including the tables
of OAuth Server.
3. Sign in
NUXT_ACL_ADMINS in apps/web/.env before you start the app. An email added later counts after a
restart of pnpm dev.- Open
http://localhost:3000, choose Create account and sign up with the email you put inNUXT_ACL_ADMINS. Signing up sends a verification email (verifyEmailis on), printed in the terminal that runspnpm dev: open its link and press Continue to sign in. The list only counts for a verified address. /adminis the admin dashboard./admin/oauthlists the OAuth applications. The mobile app'smobileapplication is marked Managed in nuxt.config. On Security (/account/security) you can add a passkey, which the mobile app's sign-in then offers too.- Open
http://localhost:3001, the mobile app in a browser. Its'/**': { authenticated: true }route rule sends you to its/loginpage. - Sign in sends you to
apps/web, which asks you to approve the app. It then returns tohttp://localhost:3001/auth/callback. - On Account, Sign out revokes the refresh token and returns to
/. The route rule sends you on to the sign-in page.
useUserSession().user, from the ID token) and the result of
GET /api/me, called cross-origin with the access token. A reload keeps you signed in.Both apps use the same app API: useUserSession(), useAuth() and $api / useApi().
apps/webruns Auth in server mode: a cookie session.apps/mobileruns it in client mode: tokens on the device.
apps/web checks every
API request.Emails such as password resets are printed in the terminal that runs pnpm dev, by the log driver of
Notify. They stay there until you configure a real driver.
4. Run it on the iOS simulator
Keep the web app running (pnpm dev, or pnpm dev:web). Then, in a second terminal:
pnpm mobile:ios
The script creates the native projects the first time: cap add ios and cap add android, plus the URL scheme
com.example.mobile in Info.plist and AndroidManifest.xml. It builds the SPA, syncs it into ios/ and asks which
simulator to run.
The simulator shares your Mac's network, so the app reaches http://localhost:3000. Sign in opens the in-app
browser at apps/web. It has its own cookies, so you sign in there once. After you approve, the redirect to
com.example.mobile:/auth/callback returns to the app, signed in.
pnpm mobile:sync after every change to the app or its .env, and after every pnpm install.5. Run it on an iPhone
A phone cannot reach localhost on your Mac. Start the web app on every interface, and build the app against your
Mac's local network (LAN) address:
pnpm dev:lan # apps/web on 0.0.0.0:3000
pnpm mobile:ios:device # MOBILE_LAN=1 build, then opens Xcode
MOBILE_LAN=1 rewrites localhost in the issuer and API base to this Mac's LAN address. MOBILE_LAN=192.168.1.20
picks the address. OAuth Server takes its issuer from the request, so it matches.
In Xcode:
- Select the App target, then Signing & Capabilities, and pick your team. A free Apple ID works, but the install then expires after 7 days. Do not commit the team id.
- Connect the phone and turn on Developer Mode on it (Settings, Privacy & Security). Choose it as the run destination and press Run.
- The first time, trust the developer profile on the phone (Settings, General, VPN & Device Management). Allow local network access when asked.
MOBILE_RELEASE=1 is
also set.6. Run it on Android
pnpm mobile:android
The steps are the same as for iOS.
https://localhost, so calls to the plain http dev server are mixed content. Test Android
against an https deployment of apps/web, or use live reload (see Device development).What is in the starter
apps/mobile is the mobile starter's app, file for file. apps/web is the workspace starter's app, except for the
files below. The
workspace starter and the
mobile starter list those files. This starter adds:
| Path | What it does |
|---|---|
apps/web/nuxt.config.ts | Adds OAuth Server and registers the mobile app as a configured client. |
apps/web/server/api/me.get.ts | The API route the mobile app calls. |
apps/web/.env.example | Adds NUXT_OAUTH_SERVER_PRIVATE_KEY. |
apps/web/package.json | Adds @frigear-nu/oauth-server and @workspace/mobile. |
apps/web/server/db/migrations/ | Also creates the tables of OAuth Server. |
apps/web/AGENTS.md | Conventions that include OAuth Server. |
How the two apps fit together
apps/mobile/identity.ts is exported as @workspace/mobile/identity. apps/web depends on @workspace/mobile to read
it:
import { appName, clientId, devRedirectUri, nativeRedirectUri } from '@workspace/mobile/identity'
export default defineNuxtConfig({
modules: [
// ... the workspace starter's modules
'@frigear-nu/oauth-server',
],
oauthServer: {
cors: {
// The webviews of the native app: iOS (capacitor://localhost) and Android (https://localhost).
origins: ['capacitor://localhost', 'https://localhost'],
// Your API: the same origins may call it with `Authorization: Bearer`.
paths: ['/api/'],
},
clients: [{
id: clientId, // 'mobile'
name: appName,
redirectUris: [nativeRedirectUri], // 'com.example.mobile:/auth/callback'
devRedirectUris: [devRedirectUri], // 'http://localhost:3001/auth/callback', under `nuxt dev` only
}],
settings: { pages: { admin: true } },
},
})
| Piece | What it does |
|---|---|
oauthServer.clients | A configured client: a public OAuth application with the fixed id mobile. |
cors.origins | The webview origins may call the token, revoke, userinfo and discovery endpoints. |
cors.paths | The same origins may call /api/** with a bearer token. Cookies never travel cross-origin. |
server/api/me.get.ts | Returns an allowlisted { id, name, email } for the signed-in user. |
NUXT_OAUTH_SERVER_PRIVATE_KEY | The key that signs tokens. |
oauthServer.clients. The configured client uses PKCE (Proof Key for Code Exchange) and has no secret. It is
created in the database on the first OAuth request. The dev redirect URI, and its origin for CORS (cross-origin resource
sharing), only exist under nuxt dev, so production never accepts them. NUXT_OAUTH_SERVER_CLIENTS (JSON) replaces
the list per environment. Create it in nuxt.config
has the details.
server/api/me.get.ts. requireUser(event) accepts the session cookie and the access token. Write your API routes
the same way.
Who may do what. OAuth Server registers its permissions (oauth:read, oauth:manage), and the starter's admin
role holds them through grant: ['*'], so admins manage the applications at /admin/oauth. An application's
allowedRoles is checked against the user's roles from authorization.roles, the NUXT_ACL_ADMINS admins included,
so no plugin is needed. The mobile application has no allowedRoles, so every signed-in user may use it. An access
token holds the permissions of its scopes that the user has, frozen when it is issued, and every request is still
checked against the user's current permissions.
NUXT_OAUTH_SERVER_PRIVATE_KEY. Without it, nuxt dev keeps a dev key in
apps/web/.data/oauth-server/dev-key.pem.
NUXT_OAUTH_SERVER_PRIVATE_KEY answers 500.To register more OAuth applications, such as another app or a confidential web app, or to change this one, see Create an OAuth application.
Rename the app
Change apps/mobile/identity.ts (app id, name, client id, scopes). The
mobile starter guide lists the fields. Then:
- Restart
pnpm dev.apps/webreads the new client id and redirect URIs from the same file. The old OAuth application stays in the database as an ordinary application, so its users stay signed in. Delete it in/admin/oauthwhen you no longer need it. - Delete
apps/mobile/iosandapps/mobile/android. Then runpnpm mobile:native(orpnpm mobile:ios) to create them with the new id and URL scheme.
Commands
Run everything from the project root:
| Command | What it does |
|---|---|
pnpm prepare:env | Creates each app's .env from .env.example and fills in empty secrets. |
pnpm dev | apps/web on http://localhost:3000 and apps/mobile on http://localhost:3001. |
pnpm dev:web / pnpm dev:mobile | One of them. |
pnpm dev:lan | apps/web on every interface, for a phone on the same network. |
pnpm build | Builds every app and package. |
pnpm preview | Previews the apps/web build. |
pnpm lint / pnpm lint:fix / pnpm typecheck / pnpm test | Checks across the workspace. |
pnpm db:generate | Generates a migration after adding or upgrading a module. Commit it. |
pnpm db:migrate | Applies migrations without starting the dev server. |
pnpm mobile:native | Creates ios/ and android/ and registers the URL scheme. Safe to rerun. |
pnpm mobile:doctor | Lists missing Capacitor plugins and URL schemes. |
pnpm mobile:sync | Runs nuxt generate and cap sync. Run it after every app change. |
pnpm mobile:ios | Builds and runs on an iOS simulator. |
pnpm mobile:ios:device | Builds against this Mac's LAN address and opens Xcode, to run on a phone. |
pnpm mobile:android | Builds and runs on an Android emulator or device. |
pnpm clean | Runs each package's clean script, then removes the root node_modules and .data. |
Production checklist
Do everything in Deployment for apps/web: the database and its migrations,
NUXT_APP_SECRET, NUXT_ACL_ADMINS and a real Notify driver. Then add the items below.
NUXT_OAUTH_SERVER_PRIVATE_KEY per environment. Never reuse the one in your local .env
(Signing keys).The server:
- Set
oauthServer.issuerto the publichttpsorigin, so tokens do not depend on the request host behind a proxy. - Set
NUXT_OAUTH_SERVER_PRIVATE_KEYin your hosting platform's environment, as above. - Serve
apps/webon its finalhttpsdomain. A passkey belongs to the domain it was created on, so moving means users add theirs again.
The app:
- Set your own
appId,appNameandclientIdinidentity.ts, and recreateios/andandroid/. - Point
NUXT_PUBLIC_AUTHENTICATION_CLIENT_ISSUERandNUXT_PUBLIC_AUTHENTICATION_CLIENT_API_BASEat the production server in the environment of the release build. Build withMOBILE_RELEASE=1. - Keep
@aparajita/capacitor-secure-storageinstalled, and runpnpm mobile:doctor. - Replace the icon and splash screen, set the version and build number, and set up signing for the App Store and Google Play.
The Mobile release checklist and the OAuth Server page have the details.
Next steps
- Add a module – add a feature to
apps/web, with the same steps as any app. - OAuth Server – scopes, claims, roles and the admin pages for OAuth applications.
- Auth in client mode – the app's sign-in, tokens and options.
- UX patterns – follow the shared patterns when you add screens.