tutorial··5 min read·WoWSQL Team

React Native Postgres Backend with the WoWSQL SDK

Set up a React Native Postgres backend with @wowsql/sdk: the Metro fix it needs, safe keys, sign-in that survives restarts, queries and realtime updates.

A React Native app can't talk to PostgreSQL directly, and it shouldn't try. Postgres drivers such as pg need Node's TCP and TLS modules, which React Native doesn't have, and even if they worked you'd be shipping database credentials inside your APK or IPA. The usual answer is a Postgres backend in between: an HTTPS API, auth and access rules in front of the database. This guide sets one up with WoWSQL's TypeScript SDK, @wowsql/sdk on npm, including the one bundler setting you need and how to keep users signed in across app restarts.

Key facts

  • Package: @wowsql/sdk 3.10.0 (Elastic License 2.0). Its only runtime dependency is axios.
  • Metro needs one setting: when we bundled the SDK with Metro on 24 October 2026, it failed with "Unable to resolve module fs". Mapping fs and path to empty modules in metro.config.js fixed it (step 2).
  • Query pattern: new WowSQLClient({ projectUrl, apiKey }).table('notes').select('*').get().
  • Keys: only the anon key (wowsql_anon_…) goes in the app. The service key (wowsql_service_…) bypasses Row Level Security and must stay on a server.
  • Auth tokens: ProjectAuthClient takes a storage option with synchronous get and set methods, so you load saved tokens before creating it.
  • Regions and pricing: Mumbai (the default) or US East 1. Free plan, then $25 or $99 a month.

Why a backend-as-a-service for a mobile app

You have two common choices. You can write your own API server (Express, FastAPI, or similar) next to a Postgres database, and own the auth, uploads and change notifications yourself. Or you can use a Postgres backend-as-a-service, which puts an auto-generated REST API, auth, file storage and realtime in front of the database for you.

The second route suits small teams shipping a mobile app, because the phone only ever makes HTTPS and WebSocket calls with a public key. React Native already has both fetch and a global WebSocket, which is all the SDK needs for tables and realtime. Under the hood, table calls are standard PostgREST requests to https://<slug>.wowsqlconnect.com/rest/v1, which our instant REST API guide explains in more detail.

1. Create a project and a table

Create a free project at app.wowsql.com and pick Mumbai or US East 1. If most of your users are in India, Mumbai keeps the round trip short. In the SQL editor, create a table:

create table public.notes (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  body text,
  created_at timestamptz not null default now()
);

Use a UUID primary key. WoWSQL's docs say raw SQL table creation doesn't accept SERIAL PRIMARY KEY. Then copy the project slug and the anon key from Settings → API.

2. Install the SDK and fix Metro

npx expo install @react-native-async-storage/async-storage
npm install @wowsql/sdk

Version 3.10.0 of the SDK has two Node-only helpers in its storage module (upload from a file path, download to a file path). They load Node's fs and path modules, and Metro tries to resolve those at build time even though your app never calls them. Tell Metro to treat them as empty modules. For an Expo project, metro.config.js looks like this:

const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

config.resolver.resolveRequest = (context, moduleName, platform) => {
  if (moduleName === 'fs' || moduleName === 'path') {
    return { type: 'empty' };
  }
  return context.resolveRequest(context, moduleName, platform);
};

module.exports = config;

In a bare React Native project, add the same resolveRequest function to the resolver section of your existing config. Restart Metro with a clean cache (npx expo start -c) after the change.

3. Create the client

Put the slug and anon key in environment variables. Expo only exposes variables that start with EXPO_PUBLIC_ to app code, and anything with that prefix ends up in the app bundle, so it must never be the service key.

// lib/wowsql.ts
import WowSQLClient from '@wowsql/sdk';

export const db = new WowSQLClient({
  projectUrl: process.env.EXPO_PUBLIC_WOWSQL_PROJECT!, // e.g. "myproject"
  apiKey: process.env.EXPO_PUBLIC_WOWSQL_ANON_KEY!,
});

4. Read and write rows

import { useEffect, useState } from 'react';
import { FlatList, Text } from 'react-native';
import { db } from './lib/wowsql';

export function NotesList() {
  const [notes, setNotes] = useState<any[]>([]);

  useEffect(() => {
    db.table('notes')
      .select('id', 'title', 'created_at')
      .order('created_at', 'desc')
      .limit(50)
      .get()
      .then(({ data }) => setNotes(data));
  }, []);

  return (
    <FlatList
      data={notes}
      keyExtractor={(n) => n.id}
      renderItem={({ item }) => <Text>{item.title}</Text>}
    />
  );
}

Writes use the same table object: db.table('notes').insert({ title: 'Hello' }), db.table('notes').update(id, { title: 'Edited' }) and db.table('notes').delete(id). For long lists, paginate(page, perPage) returns one page at a time.

5. Sign users in and keep them signed in

ProjectAuthClient handles email and password, OTP codes, magic links, OAuth and TOTP two-factor. By default it keeps tokens in memory, so a user who closes the app would have to sign in again. Its storage option fixes that, but the methods are synchronous, while AsyncStorage is asynchronous. The simple pattern is an in-memory cache that you fill from AsyncStorage once at startup and write back to on every change:

// lib/auth.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { ProjectAuthClient, AuthTokenStorage } from '@wowsql/sdk';

const cache = { access: null as string | null, refresh: null as string | null };

function persist(key: string, value: string | null) {
  value ? AsyncStorage.setItem(key, value) : AsyncStorage.removeItem(key);
}

const tokenStore: AuthTokenStorage = {
  getAccessToken: () => cache.access,
  setAccessToken: (t) => { cache.access = t; persist('wowsql.access', t); },
  getRefreshToken: () => cache.refresh,
  setRefreshToken: (t) => { cache.refresh = t; persist('wowsql.refresh', t); },
};

export async function createAuth() {
  cache.access = await AsyncStorage.getItem('wowsql.access');
  cache.refresh = await AsyncStorage.getItem('wowsql.refresh');
  return new ProjectAuthClient({
    projectUrl: process.env.EXPO_PUBLIC_WOWSQL_PROJECT!,
    apiKey: process.env.EXPO_PUBLIC_WOWSQL_ANON_KEY!,
    storage: tokenStore,
  });
}

Call createAuth() before you render your signed-in screens, because the client reads the stored token when it's constructed. Then auth.signIn({ email, password }) signs a user in, auth.getUser() checks the saved session, auth.refreshSession() renews it and auth.logout() signs out. For tokens on a shared or rooted device, consider an encrypted store such as expo-secure-store instead of AsyncStorage.

About Row Level Security. In 3.10.0, WowSQLClient sends the API key you give it on table requests, and the docs don't yet show how to run a table query as the signed-in user. If you need per-user rules such as "users see only their own notes", put those writes behind a small server endpoint that checks the user's token first, or ask us how to set it up on your plan.

6. Live updates

useEffect(() => {
  const sub = db.realtime.subscribe(
    { schema: 'public', table: 'notes', event: 'INSERT' },
    (change) => console.log('new note', change),
  );
  return () => { sub.unsubscribe(); };
}, []);

Enable realtime for the table in the dashboard first. The SDK passes the anon key in the WebSocket URL, which is one more reason the service key never belongs in the app. Payloads are capped at 64 KiB, so send IDs and refetch large rows.

7. File uploads

The SDK's WowSQLStorage upload helper checks for Node's Buffer, which React Native doesn't define by default. Until that changes, test uploads on a real device before you rely on them. The safest route is to send the file to your own server endpoint and upload it from there, which also keeps private buckets private.

When this setup fits

It fits when you want Postgres, auth, realtime and a REST API without running your own API server, and when you want your data in Mumbai or US East 1. It fits less well if you need offline-first sync. Neither the SDK nor this guide covers that, so look at a dedicated sync layer.

Building for more than one platform? The same project works with our Flutter Postgres backend guide and the Next.js Postgres backend guide. Comparing providers first? Read our look at the Indian Supabase alternative. WoWSQL is built in India, is DPIIT-recognised and is part of NVIDIA Inception.

FAQ

Can React Native connect directly to PostgreSQL?

Not in a useful way. Drivers like pg depend on Node's networking modules, which React Native doesn't include, and a direct connection would put database credentials inside your app. Use an HTTPS API in front of the database instead.

Does @wowsql/sdk work with Expo?

Yes, with one Metro setting. Version 3.10.0 references Node's fs and path in two Node-only helpers, so map those to empty modules in metro.config.js as shown in step 2.

Which key do I put in a React Native app?

Only the anon key. Anything bundled into a mobile app can be extracted, and the service key bypasses Row Level Security, so it belongs on a server.

How do I keep users signed in after the app restarts?

Pass a storage object to ProjectAuthClient. Because its methods are synchronous, load saved tokens from AsyncStorage or a secure store into memory before you create the auth client.

Which regions can a WoWSQL project use?

Two: Asia Pacific (Mumbai), the default, and US East (N. Virginia). You pick the region when you create the project, and paid plans are $25 or $99 a month after the free plan.