Photos from people you actually know.
Orbit is a privacy-focused social photo sharing app that creates an intimate feed of photos from your real-world contacts. By syncing your contacts and mirroring Instagram content, Orbit shows you a chronological feed from people in your 1st and 2nd-degree networksβwithout the noise of traditional social media.
Unlike traditional social networks where you're bombarded with content from strangers, Orbit creates an intimate photo feed exclusively from:
- 1st-degree contacts: People in your phone's contact list who use Orbit
- 2nd-degree contacts: Friends of your friends (contacts of your contacts)
This creates a natural, familiar network where every photo comes from someone you know or are connected to through a mutual friend.
- Client-side hashing: Phone numbers are hashed on-device before transmission
- Zero raw data storage: Server never sees actual phone numbers
- Salt-based encryption: SHA-256 hashing with server-side salt
- Secure token storage: JWT authentication with secure storage
- Phone-based authentication: SMS OTP verification (Twilio integration ready)
- Contact discovery: Find friends already on the platform via hashed contact matching
- Instagram mirroring: Connect Instagram to automatically sync your photos
- Chronological feed: No algorithmsβjust photos from your network, newest first
- Network expansion: See content from friends-of-friends (2nd degree)
- Clean, minimalist mobile interface
- Pull-to-refresh feed
- Profile avatars with fallback initials
- Empty state guidance for new users
- Onboarding flow with skip options
- Framework: React Native with Expo SDK 51+
- Language: TypeScript (strict mode)
- Navigation: Expo Router (file-based routing)
- State Management: React Context API
- Security: expo-secure-store, expo-crypto
- Permissions: expo-contacts
- Framework: Fastify (Node.js)
- Language: TypeScript
- Database: PostgreSQL with @databases/pg
- Authentication: JWT with @fastify/jwt
- Validation: Zod schemas
- Job Queue: BullMQ + Redis (ready for background jobs)
- SMS: Twilio (integration ready, currently mocked)
- Media Storage: AWS S3 + CloudFront CDN
- Cache Layer: Redis
- OAuth: Instagram Basic Display API
orbit/
βββ api/ # Backend API
β βββ src/
β β βββ config.ts # Environment configuration
β β βββ index.ts # Fastify server setup
β β βββ db/
β β β βββ index.ts # PostgreSQL connection pool
β β β βββ schema.sql # Database schema
β β βββ middleware/
β β β βββ auth.ts # JWT authentication middleware
β β βββ routes/
β β β βββ auth.ts # Phone verification endpoints
β β β βββ contacts.ts # Contact matching endpoints
β β β βββ instagram.ts # Instagram OAuth & sync
β β β βββ feed.ts # Feed retrieval logic
β β β βββ account.ts # User account management
β β βββ services/
β β βββ phoneService.ts # OTP generation & verification
β β βββ userService.ts # User CRUD operations
β β βββ contactService.ts # Contact matching logic
β β βββ instagramService.ts # Instagram API integration
β βββ package.json
β βββ tsconfig.json
β
βββ mobile/ # React Native mobile app
β βββ app/ # Expo Router screens
β β βββ _layout.tsx # Root layout with AuthProvider
β β βββ index.tsx # Entry point / auth router
β β βββ welcome.tsx # Landing screen
β β βββ phone-input.tsx # Phone number entry
β β βββ verify-otp.tsx # OTP verification
β β βββ contacts-permission.tsx # Contact sync onboarding
β β βββ connect-instagram.tsx # Instagram connection
β β βββ feed.tsx # Main feed screen
β βββ context/
β β βββ AuthContext.tsx # Global auth state
β βββ utils/
β β βββ api.ts # API client with typed methods
β β βββ contacts.ts # Contact hashing utilities
β β βββ storage.ts # Secure storage wrapper
β βββ package.json
β βββ tsconfig.json
β
βββ README.md # You are here
- Node.js: 18+ (20+ recommended)
- PostgreSQL: 14+
- Redis: 6+ (for background jobs)
- Expo CLI:
npm install -g expo-cli - iOS Simulator or Android Emulator or Expo Go app
Create a PostgreSQL database:
psql postgres
CREATE DATABASE orbit;
\qRun the schema migration:
psql orbit < api/src/db/schema.sqlcd api
npm installCreate api/.env.local:
# Server
NODE_ENV=development
PORT=3000
API_VERSION=v1
# JWT
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRES_IN=30d
# Database
DATABASE_URL=postgresql://postgres:password@localhost:5432/orbit
# Redis
REDIS_URL=redis://localhost:6379
# Twilio (optional for development)
TWILIO_ACCOUNT_SID=your_twilio_account_sid
TWILIO_AUTH_TOKEN=your_twilio_auth_token
TWILIO_VERIFY_SERVICE_SID=your_verify_service_sid
# Instagram Basic Display API (optional for development)
INSTAGRAM_CLIENT_ID=your_instagram_app_id
INSTAGRAM_CLIENT_SECRET=your_instagram_app_secret
INSTAGRAM_REDIRECT_URI=http://localhost:3000/v1/instagram/callback
# Contact Hashing
CONTACT_HASH_SALT=your-unique-salt-string-keep-secretnpm run devThe API will be available at http://localhost:3000
Check health: curl http://localhost:3000/health
cd mobile
npm installUpdate mobile/utils/api.ts with your local IP address (for physical devices) or appropriate emulator endpoints:
export const API_BASE_URL = Platform.select({
android: 'http://10.0.2.2:3000/v1', // Android emulator
ios: 'http://localhost:3000/v1', // iOS simulator
default: 'http://YOUR_LOCAL_IP:3000/v1' // Physical device (e.g., 192.168.1.100)
});npm startThen:
- Press
ifor iOS simulator - Press
afor Android emulator - Scan QR code with Expo Go app for physical device
Stores user accounts with hashed phone numbers and Instagram connection status.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
phone_hash |
VARCHAR(64) | SHA-256 hash of phone number |
display_name |
VARCHAR(100) | User's display name (nullable) |
avatar_url |
TEXT | Profile picture URL (nullable) |
insta_connected |
BOOLEAN | Instagram connection status |
insta_user_id |
VARCHAR(100) | Instagram user ID |
insta_access_token |
TEXT | Encrypted Instagram token |
insta_token_expires_at |
TIMESTAMP | Token expiration |
created_at |
TIMESTAMP | Account creation time |
updated_at |
TIMESTAMP | Last update time |
Stores photos from Instagram or native uploads.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
user_id |
UUID | Foreign key to users |
source |
VARCHAR(20) | 'instagram' or 'native' |
external_id |
VARCHAR(100) | Instagram media ID |
media_url |
TEXT | CDN URL of image |
caption |
TEXT | Post caption (nullable) |
taken_at |
TIMESTAMP | When photo was taken |
created_at |
TIMESTAMP | When synced to Orbit |
Maps user relationships based on contact syncing.
| Column | Type | Description |
|---|---|---|
user_id |
UUID | User who uploaded contacts |
match_user_id |
UUID | Matched user in their contacts |
created_at |
TIMESTAMP | When match was created |
Audit log of contact sync events.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
user_id |
UUID | User who uploaded |
uploaded_count |
INTEGER | Number of contacts uploaded |
created_at |
TIMESTAMP | Upload time |
Per-user privacy preferences.
| Column | Type | Description |
|---|---|---|
user_id |
UUID | Foreign key (primary) |
hide_from_contacts |
BOOLEAN | Hide profile from contacts |
mutual_only |
BOOLEAN | Show posts to mutual contacts only |
updated_at |
TIMESTAMP | Last update time |
Start phone verification by sending OTP.
Request:
{
"phone_number": "+15551234567"
}Response:
{
"success": true
}Verify OTP code and authenticate user.
Request:
{
"phone_number": "+15551234567",
"code": "123456"
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"display_name": "John Doe",
"avatar_url": null,
"phone_hash": "abc123...",
"insta_connected": false
}
}Upload hashed contacts and find matches.
Headers: Authorization: Bearer <token>
Request:
{
"hashes": ["abc123...", "def456...", ...]
}Response:
{
"matches": [
{
"id": "uuid",
"display_name": "Jane Smith",
"avatar_url": "https://...",
"phone_hash": "abc123...",
"insta_connected": true
}
],
"total_contacts": 150,
"matched_count": 12
}Get list of mutual contacts.
Headers: Authorization: Bearer <token>
Response:
{
"contacts": [
{
"id": "uuid",
"display_name": "Jane Smith",
"avatar_url": "https://...",
"phone_hash": "abc123...",
"insta_connected": true
}
]
}Exchange OAuth code for access token and connect account.
Headers: Authorization: Bearer <token>
Request:
{
"code": "instagram_oauth_code"
}Response:
{
"insta_connected": true,
"username": "johndoe",
"last_sync_at": "2025-10-26T12:00:00Z"
}Manually trigger Instagram media sync.
Headers: Authorization: Bearer <token>
Response:
{
"success": true,
"synced_at": "2025-10-26T12:00:00Z"
}Disconnect Instagram and delete synced posts.
Headers: Authorization: Bearer <token>
Response:
{
"insta_connected": false
}Get chronological feed from network.
Headers: Authorization: Bearer <token>
Query Params:
cursor(optional): ISO timestamp for paginationlimit(optional): Number of posts (default: 20)
Response:
{
"items": [
{
"id": "uuid",
"user": {
"id": "uuid",
"display_name": "Jane Smith",
"avatar_url": "https://...",
"phone_hash": "abc123..."
},
"source": "instagram",
"media_url": "https://cdn.orbit.photo/...",
"caption": "Beautiful sunset π
",
"taken_at": "2025-10-25T18:30:00Z",
"created_at": "2025-10-26T10:00:00Z"
}
],
"next_cursor": "2025-10-25T18:30:00Z",
"network_size": 45
}- Client-side hashing: Phone numbers are hashed using SHA-256 with a salt before leaving the device
- No raw storage: Server never stores or logs raw phone numbers
- Hashed matching: Contact discovery uses hash matching, preserving privacy
- Salt rotation: Server-side salt can be rotated to invalidate old hashes
- JWT tokens: Stateless authentication with 30-day expiration
- Secure storage: Tokens stored in device keychain/keystore via expo-secure-store
- Bearer token: Standard
Authorization: Bearer <token>header
- Parameterized queries: All SQL queries use parameterized inputs to prevent injection
- Zod validation: Request bodies validated with strict schemas
- CORS enabled: Cross-origin protection configured
- Instagram tokens: Encrypted at rest (implementation pending)
- Hide profile from contacts who have your number
- Mutual-only posting (only show posts to mutual contacts)
- Block list
- Account deletion with full data wipe
- Phone authentication flow (mock OTP)
- Contact hashing and matching
- Instagram connection (mock OAuth)
- Feed generation with 1st & 2nd degree network
- Basic mobile UI/UX
- JWT authentication
- Database schema with indexes
- Real Twilio SMS integration
- Real Instagram OAuth flow with WebView
- Profile customization (display name, avatar)
- Native photo uploads (camera/gallery)
- Push notifications for new posts
- Background Instagram sync (BullMQ jobs)
- Story-style ephemeral posts
- Photo reactions/comments
- Privacy settings UI
- Account deletion flow
- S3 + CloudFront media hosting
- Image optimization pipeline
- Admin dashboard
- Analytics & monitoring
- OTP is logged to console (mock mode only)
- Instagram OAuth uses mock flow
- No rate limiting on API endpoints
- Missing error boundaries in mobile app
- Network requests not cached
- No offline support
-
Sign Up:
- Start app β Welcome screen
- Enter phone number
- Use OTP from API logs
- Grant contacts permission
- Connect Instagram (mock)
-
Verify Database:
-- Check user creation SELECT * FROM users; -- Check contact matches SELECT * FROM contact_matches; -- Check synced posts SELECT * FROM posts;
-
Test Feed:
- Pull to refresh
- Verify posts from network
- Check empty state
# Health check
curl http://localhost:3000/health
# Start verification
curl -X POST http://localhost:3000/v1/auth/phone/start \
-H "Content-Type: application/json" \
-d '{"phone_number": "+15551234567"}'
# Verify OTP (check API logs for code)
curl -X POST http://localhost:3000/v1/auth/phone/verify \
-H "Content-Type: application/json" \
-d '{"phone_number": "+15551234567", "code": "123456"}'
# Get feed (use token from verify response)
curl http://localhost:3000/v1/feed \
-H "Authorization: Bearer YOUR_TOKEN"- TypeScript strict mode - No
anytypes without justification - Functional components - Use hooks, avoid class components
- Async/await - Prefer over raw promises
- Error handling - Always use try-catch for async operations
- Descriptive names - Variables and functions should be self-documenting
- Zod validation - All API inputs must be validated
- Create feature branch from
main - Use conventional commits:
feat: Add user profile editingfix: Resolve contact sync crashdocs: Update API documentationrefactor: Extract feed service
- Open PR with description
- Require review before merge
- Mobile: Feature-based folders in
/app - API: Controllers (routes), services, repositories pattern
- Shared: Types in
/shared(to be created)
Currently unlicensed. All rights reserved.
Built with modern tools:
Built with β€οΈ for authentic connections in a noisy digital world.
For questions or suggestions, open an issue or reach out to the maintainers.