Back to guides

How to use Own Auth in Ionic and Capacitor apps


Own Auth works well with Ionic and Capacitor, but the integration boundary is different from a browser-only auth SDK. Own Auth runs in your backend, where it can safely access Postgres and your token pepper. The Capacitor app calls your backend over HTTPS and keeps only the session token it receives after authentication.

Email and password authentication needs no native plugin. Magic links need deep-link handling so the email can reopen the app. Social sign-in can use a native provider plugin, but your backend must verify the provider token before passing the verified identity to Own Auth.

How the integration works

Architecture
Ionic or Capacitor app
-> your HTTPS auth endpoint
-> own-auth on your backend
-> your Postgres database

Auth emails
-> Own Auth email provider
-> app link or go.own-auth.com hosted link
-> Capacitor app
-> your backend verifies the one-time token

Prerequisites

  • An Ionic app with Capacitor configured
  • A Node.js 18 or later backend, or a Cloudflare Worker with Node.js compatibility enabled
  • A Postgres database
  • HTTPS for every production request
  • A secure storage implementation for session tokens on iOS and Android

Set up Own Auth in your backend

Install Own Auth in the backend project, not in the Ionic app:

Backend terminal
npm install own-auth
npx own-auth migrate

Set the database connection and a long random token pepper in the backend environment. Keep both values server-only.

.env
DATABASE_URL=postgres://user:password@localhost:5432/myapp
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secret

Create one shared Own Auth instance. The base URL is used for auth links. Add only destinations controlled by your application to the redirect allowlist.

auth.ts
import { createOwnAuth } from "own-auth";

export const auth = createOwnAuth({
  tokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER,
  baseUrl: "https://app.example.com",
  redirectAllowlist: [
    "https://app.example.com",
    "myapp://home",
],
});

Create the mobile auth client

The Ionic app needs a small wrapper around your own API. This example uses bearer sessions because they are straightforward across a native WebView and a separate API domain. sessionStore represents an OS-backed secure storage implementation.

auth-api.ts
const API_URL = "https://api.example.com";

type SessionResponse = {
user: { id: string; email: string; name: string | null };
sessionToken: string;
};

export async function authRequest(
path: string,
body: Record<string, unknown>,
) {
  const response = await fetch(`${API_URL}${path}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(body),
});

if (!response.ok) throw await response.json();

const result = (await response.json()) as SessionResponse;
await sessionStore.set(result.sessionToken);
return result.user;
}

export async function authenticatedFetch(path: string, init?: RequestInit) {
const sessionToken = await sessionStore.get();
  return fetch(`${API_URL}${path}`, {
  ...init,
  headers: {
    ...init?.headers,
    Authorization: `Bearer ${sessionToken}`,
  },
});
}

Email and password authentication

Password authentication is ordinary HTTPS traffic, so it does not need a Capacitor plugin. The app sends credentials to your backend, and the backend calls Own Auth.

Sign up

signup.ts
const user = await authRequest("/auth/signup", {
name: "Ada Lovelace",
email: "ada@example.com",
password: "a-long-unique-password",
});

The backend endpoint calls signUpEmailPassword and returns the new user with the opaque session token over HTTPS.

signup-handler.ts
const result = await auth.signUpEmailPassword({
name: body.name,
email: body.email,
password: body.password,
});

return {
user: result.user,
sessionToken: result.sessionToken,
};

Sign in

signin.ts
const user = await authRequest("/auth/signin", {
email: "ada@example.com",
password: "a-long-unique-password",
});

The sign-in endpoint calls signInEmailPassword. If the email or password is wrong, return the same safe invalid-credentials response for both cases so the endpoint does not reveal whether an account exists.

signin-handler.ts
const result = await auth.signInEmailPassword({
email: body.email,
password: body.password,
});

return {
user: result.user,
sessionToken: result.sessionToken,
};

Verify sessions on protected endpoints

Read the bearer token on every protected backend request and ask Own Auth for the current session. Do not trust a user ID sent by the mobile app.

require-session.ts
const header = request.headers.get("Authorization");
const sessionToken = header?.startsWith("Bearer ")
? header.slice("Bearer ".length)
: null;

const current = sessionToken
? await auth.getCurrentSession(sessionToken)
: null;

if (!current) {
return new Response("Unauthorized", { status: 401 });
}

// Use current.user.id for all authorised work.

Magic links in a Capacitor app

A magic link starts with a request from the app. Your backend calls requestMagicLink and always returns the same generic confirmation. Own Auth creates the one-time token and sends the email through your configured email provider.

request-magic-link.ts
await auth.requestMagicLink({ email: body.email });

return { message: "If that address can sign in, a link is on its way." };

Email clients handle HTTPS links more reliably than custom app schemes. For mobile apps, Own Auth Delivery can send a go.own-auth.com hosted link that opens your configured app destination and preserves the one-time token. The hosted page only bridges into the app. It does not verify the token or create a session.

Use Capacitor's App plugin to handle both a cold launch and a link received while the app is open. Send the token to your backend immediately. Do not render it, persist it, or write it to logs.

mobile-links.ts
import { App } from "@capacitor/app";

async function handleAuthLink(url?: string) {
if (!url) return;

const token = new URL(url).searchParams.get("token");
if (!token) return;

const user = await authRequest("/auth/magic-link/verify", { token });
// Route to the signed-in area of the app.
}

const launch = await App.getLaunchUrl();
await handleAuthLink(launch?.url);

App.addListener("appUrlOpen", ({ url }) => {
void handleAuthLink(url);
});

The verification endpoint consumes the token exactly once and returns the new session to the app.

verify-magic-link.ts
const result = await auth.verifyMagicLink({ token: body.token });

return {
user: result.user,
sessionToken: result.sessionToken,
};

Social sign-in with native plugins

Apple, Google, and other identity providers can use a native Capacitor plugin or a system-browser OAuth flow. The native code obtains an ID token or authorisation code. Send that credential to your backend. Do not extract its claims in the app and treat them as verified identity data.

google-signin.ts
const nativeResult = await nativeGoogleSignIn();

const user = await authRequest("/auth/social/google", {
idToken: nativeResult.idToken,
});

The backend verifies the token using the provider's official verification library. Check the signature, issuer, audience, expiry, and nonce where applicable. Only then pass the verified provider identity to Own Auth.

google-signin-handler.ts
const googleUser = await verifyGoogleIdToken(body.idToken);

const result = await auth.signInWithExternalProvider({
provider: "google",
providerAccountId: googleUser.sub,
email: googleUser.email,
emailVerified: googleUser.email_verified === true,
});

return {
user: result.user,
sessionToken: result.sessionToken,
};

Sign out

Revoke the session in the backend before deleting the local copy. If the network request fails, clear the local token anyway so the user is signed out on that device.

signout.ts
const sessionToken = await sessionStore.get();

try {
await fetch("https://api.example.com/auth/signout", {
  method: "POST",
  headers: { Authorization: `Bearer ${sessionToken}` },
});
} finally {
await sessionStore.remove();
}

Production checklist

  • Use HTTPS for the API and every auth callback
  • Keep the database URL, token pepper, and provider secrets on the backend
  • Store mobile session tokens in the iOS Keychain or Android Keystore
  • Verify every provider token on the backend before calling signInWithExternalProvider
  • Allow only your real Capacitor and web origins in CORS
  • Allowlist every magic-link redirect destination
  • Never log passwords, session tokens, provider tokens, or magic-link URLs
  • Return generic responses for sign-in and magic-link request failures

FAQ

Do I install Own Auth in the Ionic app?

No. Install Own Auth in your backend. The Ionic app calls your HTTPS endpoints and never receives database credentials or the token pepper.

Do email and password flows need a Capacitor plugin?

No. Sign-up and sign-in are ordinary HTTPS requests. A native plugin is useful only for features such as secure storage, deep-link handling, or native social sign-in.

Should a Capacitor app use cookies or bearer tokens?

Either can work. HttpOnly cookies are a strong default when the app and API share a compatible web origin. A bearer token in OS-backed secure storage is often simpler when a native WebView calls a separate API domain. Whichever approach you choose, verify the session with Own Auth on every protected request.

Can Own Auth Delivery open the native app from a magic-link email?

Yes. Configure Own Auth hosted links with the app's deep-link destination. The email uses an HTTPS go.own-auth.com link, which bridges into the app with the one-time token. The app sends that token to its backend, where Own Auth verifies it and creates the session.

Who verifies an Apple or Google ID token?

Your backend does, using the provider's supported verification library and your provider configuration. After verification, pass the trusted account ID and email claims to signInWithExternalProvider.

Conclusion

Using Own Auth with Ionic and Capacitor keeps the mobile layer small. The app collects credentials, handles native links or provider UI, and stores an opaque session token securely. Your backend owns the sensitive work: password hashing, provider-token verification, one-time token consumption, session creation, and access to Postgres.