Android / Expo

Connecting an Expo App to a Real PostgreSQL Backend

January 2026 9 min read
Mobile Backend

Every Expo tutorial defaults to Firebase. It's fast to set up and the Expo docs cover it well. But Firebase is a black box. You don't control the schema, the query logic, or what happens to your data. For my restaurant booking system, I needed proper relational data — tables, reservations, users, with real foreign key constraints and role-based access control. Firebase wasn't going to cut it.

Here is the actual setup I used: Node.js + Express backend, PostgreSQL database, JWT auth, and an Expo front-end consuming the REST API.

The CORS Problem on Mobile

The first thing that trips people up when connecting a mobile app to a custom backend: CORS. When you test with Expo Go on a physical device, your API requests come from a different origin than your server expects. You need to configure your Express server to allow requests from your Expo dev client's origin explicitly — and then configure it again differently for production.

I use the cors package with an environment-variable-driven whitelist. Dev allows localhost and the Expo Go tunnel URL. Production allows only the deployed app domain.

Auth With JWT

I generate a JWT on login, store it in the app with SecureStore from Expo, and attach it as a Bearer token to every protected request. On the backend, a middleware function verifies the token and attaches the decoded user object to the request before it reaches any route handler.

The key thing: the role (admin vs buyer) lives in the JWT payload. Route middleware checks the role and returns a 403 before executing any business logic. The admin routes and buyer routes are completely separate Express routers mounted at different paths.

Never trust the client to tell you who they are. The role check must happen on the server, on every request, by verifying a signed token — not by reading a value the client sent in the request body.

Environment Variables in Expo

This is the part nobody explains clearly. Expo has its own system for env vars via app.config.js and the extra field. For SDK 49+, you can use EXPO_PUBLIC_ prefixed variables in a .env file and they're automatically available in your app code via process.env. Never commit your .env file. Your API base URL, any public keys — these go here. Private secrets like your database connection string never touch the client.

Local Dev vs Production

During local development your API is running on localhost:3000. But your phone (running Expo Go) can't reach your laptop's localhost. You either use your machine's local network IP address, or you use the Expo tunnel feature which proxies through Expo's servers. I use the local IP during development — it's faster and doesn't depend on Expo's infrastructure being up.

In production, the backend is deployed to a VPS, the URL is in the environment variable, and the Expo build uses that. It's not complicated once you understand the separation — local IP for dev, real URL for prod, always from an env var, never hardcoded.

All Posts Next: AI Tools, Honestly