FROM FIREBASE - nself-org/cli GitHub Wiki
Last Updated: January 30, 2026 Migration Difficulty: High Estimated Time: 16-32 hours Compatibility: 60% (NoSQL โ SQL, major architecture shift)
- Overview
- Prerequisites
- Architecture Differences
- Schema Design Conversion
- Step-by-Step Migration
- Firestore to PostgreSQL Migration
- Authentication Migration
- Cloud Storage to MinIO Migration
- Cloud Functions to nself Functions Migration
- Realtime Database Migration
- Security Rules to Hasura Permissions
- Frontend Code Changes
- Common Pitfalls
- Rollback Procedure
- Automated Migration Tools
Migrating from Firebase to nself is the most complex migration due to fundamental architectural differences:
Firebase: NoSQL (Firestore/Realtime DB) + Proprietary services + Cloud-only nself: Relational (PostgreSQL) + Open-source stack + Self-hosted
- Escape vendor lock-in - Firebase is proprietary, nself is open-source
- Relational database power - Joins, complex queries, transactions
- Full control - Self-hosted infrastructure, own your data
- Cost predictability - No surprise bills from Firebase usage spikes
- Advanced features - Multi-tenancy, billing, plugins
- SQL expertise - Use existing SQL knowledge
| Challenge | Difficulty | Time Estimate |
|---|---|---|
| NoSQL โ SQL schema design | High | 4-8 hours |
| Data transformation & import | High | 4-8 hours |
| Authentication migration | Medium | 2-3 hours |
| Security rules โ Hasura permissions | High | 2-4 hours |
| Frontend SDK replacement | High | 4-8 hours |
| Cloud Functions conversion | Medium | 2-4 hours |
Total: 16-32 hours
- Full Firebase project backup
- Firestore data export (JSON)
- Cloud Storage inventory
- List of all Cloud Functions
- Security Rules documentation
- Authentication providers list
- nself installed
- Understanding of relational database design
# Install nself
brew install nself
# OR
curl -sSL https://install.nself.org | bash
# Install Firebase CLI
npm install -g firebase-tools
# Install PostgreSQL client
brew install postgresql # macOS
sudo apt-get install postgresql-client # Ubuntu
# Install jq for JSON processing
brew install jq
# Optional: firebase2graphql tool
npm install -g firebase2graphql# 1. Authenticate with Firebase
firebase login
# 2. Export Firestore data
firebase firestore:export gs://[YOUR-BUCKET]/firestore-export
# 3. Export Authentication users
# From Firebase Console โ Authentication โ Users โ Export Users
# 4. List Cloud Functions
firebase functions:list
# 5. Export Cloud Storage files
# Use Firebase Console or gsutil
# 6. Document all Security Rules
firebase deploy --only firestore:rules --dry-runFirebase Firestore (NoSQL):
// Document structure (denormalized)
{
"posts": {
"post1": {
"title": "Hello",
"author": {
"id": "user1",
"name": "John",
"email": "[email protected]" // Duplicated data
},
"comments": [
{ "text": "Nice!", "user": "Alice" },
{ "text": "Great!", "user": "Bob" }
]
}
}
}nself PostgreSQL (Relational):
-- Normalized schema
CREATE TABLE users (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE posts (
id UUID PRIMARY KEY,
title TEXT NOT NULL,
author_id UUID REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE comments (
id UUID PRIMARY KEY,
post_id UUID REFERENCES posts(id),
user_id UUID REFERENCES users(id),
text TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);Firebase SDK:
import { getFirestore, collection, query, where, getDocs } from 'firebase/firestore'
const db = getFirestore()
const q = query(collection(db, 'posts'), where('published', '==', true))
const snapshot = await getDocs(q)nself GraphQL:
query GetPosts {
posts(where: { published: { _eq: true } }) {
id
title
author {
name
}
}
}Analyze your Firestore collections and documents:
# Export schema analysis
firebase firestore:export gs://[YOUR-BUCKET]/firestore-export
gsutil -m cp -r gs://[YOUR-BUCKET]/firestore-export .
# Analyze structure
cat firestore-export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata | jqExample Firebase structure:
Collections:
- users
- uid: string
- email: string
- displayName: string
- photoURL: string
- posts: array (subcollection reference)
- posts
- postId: string
- title: string
- content: string
- authorId: string
- authorName: string (denormalized)
- tags: array
- createdAt: timestamp
- comments: subcollection
- posts/{postId}/comments
- commentId: string
- text: string
- userId: string
- userName: string (denormalized)
Convert to normalized PostgreSQL schema:
schema.sql:
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
firebase_uid TEXT UNIQUE, -- For migration mapping
email TEXT UNIQUE NOT NULL,
display_name TEXT,
photo_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Posts table
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
firebase_id TEXT UNIQUE, -- For migration mapping
title TEXT NOT NULL,
content TEXT,
author_id UUID REFERENCES users(id) ON DELETE CASCADE,
published BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tags table (normalize array โ table)
CREATE TABLE tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT UNIQUE NOT NULL
);
-- Post tags junction table
CREATE TABLE post_tags (
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
tag_id UUID REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
-- Comments table
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
firebase_id TEXT UNIQUE,
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
text TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
CREATE INDEX idx_comments_post ON comments(post_id);
CREATE INDEX idx_comments_user ON comments(user_id);Document how Firebase documents map to PostgreSQL tables:
migration-mapping.md:
# Firebase โ PostgreSQL Mapping
## Collections โ Tables
- `users` collection โ `users` table
- `uid` (Firebase) โ `firebase_uid` (tracking) + `id` (new UUID)
- `email` โ `email`
- `displayName` โ `display_name`
- `photoURL` โ `photo_url`
- `posts` collection โ `posts` table
- Document ID โ `firebase_id` + `id` (new UUID)
- `authorId` โ `author_id` (FK to users)
- `authorName` โ REMOVED (query via join)
- `tags[]` โ `post_tags` junction table
- `posts/{postId}/comments` subcollection โ `comments` table
- `userId` โ `user_id` (FK to users)
- `userName` โ REMOVED (query via join)
## Denormalization โ Normalization
- Author names stored in posts โ JOIN with users table
- Commenter names in comments โ JOIN with users table
- Tags array โ Separate tags table + junction table# 1. Initialize project
mkdir firebase-to-nself && cd firebase-to-nself
nself init --wizard
# 2. Configure .env
nano .env.env Configuration:
PROJECT_NAME=firebase-migration
ENV=dev
BASE_DOMAIN=localhost
# Database
POSTGRES_DB=firebase_migration
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-secure-password
# Hasura
HASURA_GRAPHQL_ADMIN_SECRET=your-admin-secret
HASURA_GRAPHQL_JWT_SECRET={"type":"HS256","key":"your-jwt-secret-min-32-chars"}
# Auth
AUTH_SERVER_URL=http://auth.localhost
AUTH_CLIENT_URL=http://localhost:3000
# Storage
MINIO_ENABLED=true
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
# Services
REDIS_ENABLED=true
FUNCTIONS_ENABLED=true
MAILPIT_ENABLED=true# 3. Build and start
nself build
nself start
# 4. Verify
nself doctor# 1. Create schema file
nano schema.sql
# (paste schema from Step 2 above)
# 2. Apply schema
nself db shell < schema.sql
# 3. Verify tables created
nself db shell\dt -- List tables
\d users -- Describe users table
\d posts
\d comments
\q# Export to Google Cloud Storage
firebase firestore:export gs://[YOUR-BUCKET]/firestore-backup
# Download to local
gsutil -m cp -r gs://[YOUR-BUCKET]/firestore-backup ./firestore-export
# Convert to JSON (if needed)
# Use Firebase Admin SDK or firebase2graphql toolExample transformation script:
// firebase-to-sql.js
const admin = require('firebase-admin')
const fs = require('fs')
admin.initializeApp({
credential: admin.credential.cert(require('./serviceAccountKey.json'))
})
const db = admin.firestore()
async function exportToSQL() {
const users = []
const posts = []
const comments = []
const tags = new Set()
const postTags = []
// Export users
const usersSnapshot = await db.collection('users').get()
usersSnapshot.forEach(doc => {
const data = doc.data()
users.push({
firebase_uid: doc.id,
email: data.email,
display_name: data.displayName || null,
photo_url: data.photoURL || null,
created_at: data.createdAt?.toDate() || new Date()
})
})
// Export posts
const postsSnapshot = await db.collection('posts').get()
for (const doc of postsSnapshot.docs) {
const data = doc.data()
posts.push({
firebase_id: doc.id,
title: data.title,
content: data.content,
author_firebase_uid: data.authorId, // Will map to UUID later
published: data.published || false,
created_at: data.createdAt?.toDate() || new Date()
})
// Extract tags
if (data.tags && Array.isArray(data.tags)) {
data.tags.forEach(tag => {
tags.add(tag)
postTags.push({
post_firebase_id: doc.id,
tag_name: tag
})
})
}
// Export comments subcollection
const commentsSnapshot = await db
.collection('posts')
.doc(doc.id)
.collection('comments')
.get()
commentsSnapshot.forEach(commentDoc => {
const commentData = commentDoc.data()
comments.push({
firebase_id: commentDoc.id,
post_firebase_id: doc.id,
user_firebase_uid: commentData.userId,
text: commentData.text,
created_at: commentData.createdAt?.toDate() || new Date()
})
})
}
// Write to JSON files
fs.writeFileSync('users.json', JSON.stringify(users, null, 2))
fs.writeFileSync('posts.json', JSON.stringify(posts, null, 2))
fs.writeFileSync('comments.json', JSON.stringify(comments, null, 2))
fs.writeFileSync('tags.json', JSON.stringify([...tags], null, 2))
fs.writeFileSync('post_tags.json', JSON.stringify(postTags, null, 2))
console.log('โ
Export complete!')
console.log(`Users: ${users.length}`)
console.log(`Posts: ${posts.length}`)
console.log(`Comments: ${comments.length}`)
console.log(`Tags: ${tags.size}`)
}
exportToSQL()# Run export script
node firebase-to-sql.js// json-to-sql.js
const fs = require('fs')
function generateInserts(tableName, records, columns) {
const inserts = []
for (const record of records) {
const values = columns.map(col => {
const val = record[col]
if (val === null || val === undefined) return 'NULL'
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`
if (val instanceof Date) return `'${val.toISOString()}'`
return val
}).join(', ')
inserts.push(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${values});`)
}
return inserts.join('\n')
}
// Load JSON files
const users = JSON.parse(fs.readFileSync('users.json'))
const posts = JSON.parse(fs.readFileSync('posts.json'))
const comments = JSON.parse(fs.readFileSync('comments.json'))
const tags = JSON.parse(fs.readFileSync('tags.json'))
const postTags = JSON.parse(fs.readFileSync('post_tags.json'))
// Generate SQL
let sql = '-- Users\n'
sql += generateInserts('users', users, ['firebase_uid', 'email', 'display_name', 'photo_url', 'created_at'])
sql += '\n\n-- Tags\n'
sql += tags.map(tag => `INSERT INTO tags (name) VALUES ('${tag}');`).join('\n')
sql += '\n\n-- Posts (with author_id from users)\n'
sql += `
UPDATE posts p
SET author_id = u.id
FROM users u
WHERE p.author_firebase_uid = u.firebase_uid;
`
// Write to file
fs.writeFileSync('import.sql', sql)
console.log('โ
SQL generated: import.sql')node json-to-sql.js# Import data
nself db shell < import.sql
# Verify counts
nself db shellSELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM posts;
SELECT COUNT(*) FROM comments;
SELECT COUNT(*) FROM tags;
-- Check relationships
SELECT p.title, u.display_name AS author
FROM posts p
JOIN users u ON p.author_id = u.id
LIMIT 10;Firebase Authentication โ nHost Auth
# From Firebase Console:
# 1. Go to Authentication โ Users
# 2. Click "Export Users" โ Download CSV
# OR use Firebase Admin SDK:// export-auth-users.js
const admin = require('firebase-admin')
const fs = require('fs')
admin.initializeApp({
credential: admin.credential.cert(require('./serviceAccountKey.json'))
})
async function exportAuthUsers() {
const listUsers = await admin.auth().listUsers()
const users = listUsers.users.map(user => ({
uid: user.uid,
email: user.email,
emailVerified: user.emailVerified,
displayName: user.displayName,
photoURL: user.photoURL,
disabled: user.disabled,
metadata: {
creationTime: user.metadata.creationTime,
lastSignInTime: user.metadata.lastSignInTime
},
providerData: user.providerData
}))
fs.writeFileSync('firebase-auth-users.json', JSON.stringify(users, null, 2))
console.log(`โ
Exported ${users.length} users`)
}
exportAuthUsers()# nHost Auth uses the same auth.users table structure
nself db shell-- Import users into auth.users table
INSERT INTO auth.users (
id,
email,
email_verified,
display_name,
avatar_url,
disabled,
created_at,
last_seen
)
SELECT
firebase_uid::uuid, -- Convert Firebase UID to UUID
email,
true, -- Assume verified (or import from Firebase data)
display_name,
photo_url,
false,
created_at,
NOW()
FROM users
WHERE firebase_uid IS NOT NULL;# Configure OAuth providers in .env (same as Firebase)
# GitHub
AUTH_PROVIDER_GITHUB_ENABLED=true
AUTH_PROVIDER_GITHUB_CLIENT_ID=your-client-id
AUTH_PROVIDER_GITHUB_CLIENT_SECRET=your-secret
# Google
AUTH_PROVIDER_GOOGLE_ENABLED=true
AUTH_PROVIDER_GOOGLE_CLIENT_ID=your-client-id
AUTH_PROVIDER_GOOGLE_CLIENT_SECRET=your-secret
# Update redirect URLs in provider dashboards:
# OLD: https://[project-id].firebaseapp.com/__/auth/handler
# NEW: http://auth.localhost/v1/auth/callback/github (dev)
# https://auth.yourdomain.com/v1/auth/callback/github (prod)Users must reset passwords (Firebase password hashes are not portable):
# Send password reset emails to all users
cat > send-reset-emails.sh << 'EOF'
#!/bin/bash
AUTH_URL="http://auth.localhost/v1"
nself db shell -c "SELECT email FROM auth.users;" | tail -n +3 | head -n -2 | while read EMAIL; do
echo "Sending reset to: $EMAIL"
curl -X POST "$AUTH_URL/user/password-reset" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$EMAIL\"}"
sleep 1
done
EOF
chmod +x send-reset-emails.sh
./send-reset-emails.shFirebase Cloud Storage โ MinIO
# Using gsutil
gsutil -m cp -r gs://[your-project].appspot.com ./firebase-storage
# OR use Firebase Admin SDK// download-storage.js
const admin = require('firebase-admin')
const fs = require('fs')
const path = require('path')
admin.initializeApp({
credential: admin.credential.cert(require('./serviceAccountKey.json')),
storageBucket: '[your-project].appspot.com'
})
const bucket = admin.storage().bucket()
async function downloadAllFiles() {
const [files] = await bucket.getFiles()
for (const file of files) {
const destPath = `./firebase-storage/${file.name}`
const destDir = path.dirname(destPath)
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
await file.download({ destination: destPath })
console.log(`Downloaded: ${file.name}`)
}
console.log(`โ
Downloaded ${files.length} files`)
}
downloadAllFiles()# Create buckets
docker exec -it $(docker ps -qf "name=minio") mc alias set local http://localhost:9000 minioadmin minioadmin
docker exec -it $(docker ps -qf "name=minio") mc mb local/default
docker exec -it $(docker ps -qf "name=minio") mc mb local/avatars
# Upload files
docker exec -it $(docker ps -qf "name=minio") mc mirror ./firebase-storage local/default
# Set bucket policies (if public)
docker exec -it $(docker ps -qf "name=minio") mc policy set download local/default-- Update image URLs from Firebase to MinIO
UPDATE posts
SET image_url = REPLACE(
image_url,
'https://firebasestorage.googleapis.com/v0/b/[project].appspot.com/o/',
'http://minio.localhost/default/'
);
UPDATE users
SET photo_url = REPLACE(
photo_url,
'https://firebasestorage.googleapis.com/v0/b/[project].appspot.com/o/',
'http://minio.localhost/avatars/'
);Firebase Security Rules โ Hasura Permissions
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can read/write their own document
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Posts
match /posts/{postId} {
allow read: if true; // Anyone can read
allow create: if request.auth != null; // Authenticated users can create
allow update, delete: if request.auth.uid == resource.data.authorId; // Only author can edit
}
// Comments
match /posts/{postId}/comments/{commentId} {
allow read: if true;
allow create: if request.auth != null;
allow update, delete: if request.auth.uid == resource.data.userId;
}
}
}Access Hasura Console:
# Open http://api.localhost
# Go to Data โ [table] โ PermissionsUsers table:
role: user
permissions:
select:
filter:
id: { _eq: X-Hasura-User-Id }
columns: [id, email, display_name, photo_url, created_at]
update:
filter:
id: { _eq: X-Hasura-User-Id }
check:
id: { _eq: X-Hasura-User-Id }
columns: [display_name, photo_url]Posts table:
role: user
permissions:
select:
filter: {} # Anyone can read (like Firebase "allow read: if true")
columns: [id, title, content, author_id, published, created_at]
insert:
check:
author_id: { _eq: X-Hasura-User-Id }
columns: [title, content, published]
update:
filter:
author_id: { _eq: X-Hasura-User-Id } # Only author
check:
author_id: { _eq: X-Hasura-User-Id }
columns: [title, content, published]
delete:
filter:
author_id: { _eq: X-Hasura-User-Id }Comments table:
role: user
permissions:
select:
filter: {} # Anyone can read
columns: [id, post_id, user_id, text, created_at]
insert:
check:
user_id: { _eq: X-Hasura-User-Id }
columns: [post_id, text]
update:
filter:
user_id: { _eq: X-Hasura-User-Id }
check:
user_id: { _eq: X-Hasura-User-Id }
columns: [text]
delete:
filter:
user_id: { _eq: X-Hasura-User-Id }Anonymous role:
role: anonymous
permissions:
select:
filter:
published: { _eq: true } # Only published posts
columns: [id, title, content, created_at]Firebase Cloud Functions โ nself Functions
// Firebase Cloud Functions (functions/src/index.ts)
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
admin.initializeApp()
// HTTP function
export const helloWorld = functions.https.onRequest((req, res) => {
res.json({ message: 'Hello from Firebase!' })
})
// Firestore trigger
export const onPostCreate = functions.firestore
.document('posts/{postId}')
.onCreate(async (snap, context) => {
const postData = snap.data()
const authorId = postData.authorId
// Send notification
await admin.firestore().collection('notifications').add({
userId: authorId,
message: `Your post "${postData.title}" was created`,
createdAt: admin.firestore.FieldValue.serverTimestamp()
})
})
// Auth trigger
export const onUserCreate = functions.auth.user().onCreate(async (user) => {
// Create user profile in Firestore
await admin.firestore().collection('users').doc(user.uid).set({
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL,
createdAt: admin.firestore.FieldValue.serverTimestamp()
})
})HTTP Function:
// functions/src/hello.ts
import { Request, Response } from 'express'
export default (req: Request, res: Response) => {
res.json({ message: 'Hello from nself!' })
}Database Trigger (via Hasura Event):
// functions/src/on-post-create.ts
import { Request, Response } from 'express'
import { GraphQLClient, gql } from 'graphql-request'
const client = new GraphQLClient(process.env.GRAPHQL_URL!, {
headers: {
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET!
}
})
export default async (req: Request, res: Response) => {
const { event } = req.body
if (event.op === 'INSERT') {
const post = event.data.new
// Create notification
const CREATE_NOTIFICATION = gql`
mutation CreateNotification($userId: uuid!, $message: String!) {
insert_notifications_one(object: { user_id: $userId, message: $message }) {
id
}
}
`
await client.request(CREATE_NOTIFICATION, {
userId: post.author_id,
message: `Your post "${post.title}" was created`
})
}
res.json({ success: true })
}Configure Hasura Event Trigger:
In Hasura Console โ Events โ Create Event Trigger:
- Table:
posts - Operations: INSERT
- Webhook URL:
http://functions:3000/on-post-create
Auth Trigger (via Hasura Event on auth.users):
Similar approach - create event trigger on auth.users table.
Before (Firebase SDK):
import { initializeApp } from 'firebase/app'
import {
getFirestore,
collection,
query,
where,
getDocs,
addDoc,
updateDoc,
deleteDoc,
doc
} from 'firebase/firestore'
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'
const app = initializeApp(firebaseConfig)
const db = getFirestore(app)
const auth = getAuth(app)
// Query posts
const postsRef = collection(db, 'posts')
const q = query(postsRef, where('published', '==', true))
const snapshot = await getDocs(q)
const posts = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))
// Create post
await addDoc(collection(db, 'posts'), {
title: 'New Post',
content: 'Content',
authorId: auth.currentUser.uid,
createdAt: serverTimestamp()
})
// Update post
await updateDoc(doc(db, 'posts', postId), {
title: 'Updated Title'
})
// Delete post
await deleteDoc(doc(db, 'posts', postId))
// Sign in
await signInWithEmailAndPassword(auth, email, password)After (nself GraphQL):
import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from '@apollo/client'
const client = new ApolloClient({
uri: 'http://api.localhost/v1/graphql',
cache: new InMemoryCache(),
headers: {
Authorization: `Bearer ${accessToken}`
}
})
// Query posts
const GET_POSTS = gql`
query GetPosts {
posts(where: { published: { _eq: true } }) {
id
title
content
author {
display_name
}
}
}
`
const { data } = useQuery(GET_POSTS)
const posts = data?.posts
// Create post
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
insert_posts_one(object: { title: $title, content: $content }) {
id
}
}
`
const [createPost] = useMutation(CREATE_POST)
await createPost({ variables: { title: 'New Post', content: 'Content' } })
// Update post
const UPDATE_POST = gql`
mutation UpdatePost($id: uuid!, $title: String!) {
update_posts_by_pk(pk_columns: { id: $id }, _set: { title: $title }) {
id
}
}
`
const [updatePost] = useMutation(UPDATE_POST)
await updatePost({ variables: { id: postId, title: 'Updated Title' } })
// Delete post
const DELETE_POST = gql`
mutation DeletePost($id: uuid!) {
delete_posts_by_pk(id: $id) {
id
}
}
`
const [deletePost] = useMutation(DELETE_POST)
await deletePost({ variables: { id: postId } })
// Sign in (use fetch or axios)
const response = await fetch('http://auth.localhost/v1/signin/email-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const { accessToken } = await response.json()Firebase Realtime:
import { onSnapshot } from 'firebase/firestore'
const unsubscribe = onSnapshot(
query(collection(db, 'posts'), where('published', '==', true)),
(snapshot) => {
const posts = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))
setPosts(posts)
}
)nself GraphQL Subscriptions:
import { useSubscription, gql } from '@apollo/client'
const POSTS_SUBSCRIPTION = gql`
subscription OnPosts {
posts(where: { published: { _eq: true } }, order_by: { created_at: desc }) {
id
title
content
author {
display_name
}
}
}
`
const { data } = useSubscription(POSTS_SUBSCRIPTION)
const posts = data?.postsProblem: Firebase document IDs are strings, PostgreSQL UUIDs are different
Solution:
-- Keep Firebase IDs for mapping
ALTER TABLE posts ADD COLUMN firebase_id TEXT;
-- Use UUIDs for new primary keys
ALTER TABLE posts ADD COLUMN id UUID PRIMARY KEY DEFAULT gen_random_uuid();Problem: Firestore supports arrays, PostgreSQL doesn't (well)
Solution: Normalize to junction table
-- Instead of tags: ['javascript', 'node']
-- Create tags table + post_tags junction tableProblem: Firebase timestamps vs PostgreSQL timestamps
Solution:
// Convert Firebase Timestamp to Date
const createdAt = firestoreDoc.data().createdAt.toDate()Problem: Firebase request.auth.uid doesn't exist in Hasura
Solution: Use Hasura session variables
# Firebase: request.auth.uid == userId
# Hasura: user_id: { _eq: X-Hasura-User-Id }If migration fails:
- Keep Firebase project active during migration
- Test nself thoroughly before DNS switch
- Keep Firebase running for 2-4 weeks as fallback
- Monitor error rates after switch
- Have rollback script ready:
# Rollback to Firebase
# 1. Change DNS back to Firebase
# 2. Revert frontend to Firebase SDK
# 3. Rebuild and deploy#!/bin/bash
# firebase-to-nself.sh
set -e
echo "๐ Firebase to nself Migration Helper"
echo "======================================"
FIREBASE_PROJECT=$1
NSELF_PROJECT=$2
if [ -z "$FIREBASE_PROJECT" ] || [ -z "$NSELF_PROJECT" ]; then
echo "Usage: ./firebase-to-nself.sh <firebase-project-id> <nself-project-name>"
exit 1
fi
echo "๐ฅ Step 1: Exporting from Firebase..."
mkdir -p migration-data
firebase firestore:export gs://$FIREBASE_PROJECT.appspot.com/export
gsutil -m cp -r gs://$FIREBASE_PROJECT.appspot.com/export ./migration-data/
echo "๐ง Step 2: Initializing nself..."
mkdir -p $NSELF_PROJECT
cd $NSELF_PROJECT
nself init --name $NSELF_PROJECT
echo "๐ฆ Step 3: Building nself..."
nself build
nself start
sleep 30
echo "โ
nself is ready!"
echo ""
echo "โ ๏ธ MANUAL STEPS REQUIRED:"
echo "1. Design PostgreSQL schema (see docs/migration/FROM-FIREBASE.md)"
echo "2. Transform Firestore data to SQL"
echo "3. Import data to PostgreSQL"
echo "4. Convert Security Rules to Hasura Permissions"
echo "5. Update frontend code (Firebase SDK โ GraphQL)"
echo ""
echo "๐ Full guide: docs/migration/FROM-FIREBASE.md"Migrating from Firebase to nself is a significant undertaking due to NoSQL โ SQL paradigm shift. However, the benefits are substantial:
- โ Relational power - Joins, complex queries, transactions
- โ No vendor lock-in - Open-source stack
- โ Full control - Own your infrastructure and data
- โ Cost savings - Predictable costs vs Firebase usage bills
Timeline: 16-32 hours for complete migration
Recommended Approach:
- Design PostgreSQL schema carefully (4-8 hours)
- Migrate to staging environment first
- Test thoroughly for 2-4 weeks
- Migrate production during low-traffic period
- Keep Firebase running for 1 month as fallback
Good luck with your migration! ๐