Documentation Index
Fetch the complete documentation index at: https://docs.shipnative.app/llms.txt
Use this file to discover all available pages before exploring further.
Shipnative’s service architecture is designed to be modular. Adding a new service follows a consistent pattern across the codebase.
Architecture Overview
Services implement platform-agnostic interfaces, making it easy to swap providers or add new ones:
apps/app/app/
├── types/ # Service interfaces
├── services/ # Service implementations
├── config/
│ ├── env.ts # Environment variables (Zod validated)
│ └── features.ts # Feature flags
└── app.tsx # Service initialization
Key principles:
- Each service implements a TypeScript interface
- Platform detection handles iOS/Android/Web differences automatically
- Services fail gracefully when credentials are missing
- Feature flags control service availability
Adding a New Service
Let’s walk through adding a hypothetical service. The same pattern applies whether you’re adding OneSignal, Firebase, Convex, Amplitude, or any other provider.
Step 1: Define the Interface
Create a type definition in apps/app/app/types/:
// apps/app/app/types/newservice.ts
export type NewServicePlatform = "newservice"
export interface NewServiceConfig {
apiKey: string
// Add platform-specific options as needed
}
export interface NewServiceInterface {
platform: NewServicePlatform
initialize(config: NewServiceConfig): Promise<void>
// Define your service methods
doSomething(data: unknown): Promise<void>
// Optional cleanup
destroy?(): Promise<void>
}
Export from the types index:
// apps/app/app/types/index.ts
export * from "./newservice"
Step 2: Add Environment Variables
Update the Zod schema in apps/app/app/config/env.ts:
const EnvSchema = z.object({
// ... existing variables
// New service
newserviceApiKey: z.string().optional(),
})
Add to the isServiceConfigured function:
export function isServiceConfigured(
service: "supabase" | "revenuecat" | "posthog" | "sentry" | "newservice"
): boolean {
switch (service) {
// ... existing cases
case "newservice":
return !!env.newserviceApiKey
}
}
Step 3: Add Feature Flag
Update apps/app/app/config/features.ts:
export interface FeatureFlags {
// ... existing flags
enableNewService: boolean
}
function getFeatureFlags(): FeatureFlags {
return {
// ... existing flags
enableNewService: isServiceConfigured("newservice"),
}
}
Step 4: Create the Service
Create the service implementation in apps/app/app/services/:
// apps/app/app/services/newservice.ts
import { Platform } from "react-native"
import { env } from "../config/env"
import type { NewServiceInterface, NewServiceConfig } from "../types/newservice"
import { logger } from "../utils/Logger"
// Platform-specific SDK loading
let SDK: typeof import("newservice-sdk") | null = null
if (Platform.OS !== "web") {
try {
SDK = require("newservice-react-native")
} catch {
if (__DEV__) logger.warn("NewService SDK not available")
}
} else {
// Web SDK loaded differently if needed
}
class NewService implements NewServiceInterface {
platform = "newservice" as const
private initialized = false
async initialize(config: NewServiceConfig): Promise<void> {
if (this.initialized) return
try {
SDK?.configure(config.apiKey)
this.initialized = true
logger.info("NewService initialized")
} catch (error) {
logger.error("NewService init failed", {}, error as Error)
}
}
async doSomething(data: unknown): Promise<void> {
if (!this.initialized) {
logger.warn("NewService not initialized")
return
}
SDK?.doSomething(data)
}
async destroy(): Promise<void> {
// Cleanup if needed
}
}
// Export singleton
export const newservice: NewServiceInterface = new NewService()
// Export init function
export function initNewService(): void {
if (!env.newserviceApiKey) {
if (__DEV__) {
logger.info("NewService not configured - skipping")
}
return
}
newservice.initialize({
apiKey: env.newserviceApiKey,
})
}
Step 5: Initialize in App
Add initialization to apps/app/app/app.tsx:
import { initNewService } from "./services/newservice"
// Inside useEffect with other service inits
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
// ... existing inits
initNewService()
})
}, [])
Step 6: Use the Service
import { newservice } from "@/services/newservice"
import { features } from "@/config/features"
function MyComponent() {
useEffect(() => {
if (features.enableNewService) {
newservice.doSomething({ key: "value" })
}
}, [])
}
Quick Reference: Files to Update
| File | What to Add |
|---|
types/newservice.ts | Interface definition |
types/index.ts | Export the new types |
config/env.ts | Zod schema + isServiceConfigured |
config/features.ts | Feature flag |
services/newservice.ts | Service implementation |
app.tsx | Initialization call |
Common Integrations
Push Notifications (OneSignal)
Replace or extend the existing Expo notifications:
// types/pushNotifications.ts
export interface PushService {
initialize(): Promise<void>
requestPermission(): Promise<boolean>
subscribe(userId: string): Promise<void>
unsubscribe(): Promise<void>
}
Realtime Database (Convex)
Add alongside or instead of Supabase realtime:
// types/realtime.ts
export interface RealtimeService {
connect(): Promise<void>
subscribe<T>(query: string, callback: (data: T) => void): () => void
mutate(mutation: string, args: unknown): Promise<void>
}
Analytics (Amplitude, Mixpanel)
The existing AnalyticsService interface works for most providers:
// Just implement the existing interface
import type { AnalyticsService } from "../types/analytics"
class AmplitudeService implements AnalyticsService {
platform = "amplitude" as const
// ... implement methods
}
Replacing Existing Services
To swap out a service (e.g., PostHog → Amplitude):
- Create new implementation using the same interface
- Update the export in the service file
- No other code changes needed
// services/analytics.ts
import { AmplitudeService } from "./amplitude"
import { PostHogService } from "./posthog"
// Swap by changing this line
export const analytics: AnalyticsService = new AmplitudeService()
// export const analytics: AnalyticsService = new PostHogService()
Many services have different SDKs for mobile and web:
let MobileSDK: typeof import("service-react-native") | null = null
let WebSDK: typeof import("service-js") | null = null
if (Platform.OS === "web") {
import("service-js").then((module) => {
WebSDK = module
})
} else {
try {
MobileSDK = require("service-react-native")
} catch {
// SDK not installed
}
}
// Then use the appropriate one
const sdk = Platform.OS === "web" ? WebSDK : MobileSDK
Testing Your Integration
- Without credentials: Service should skip initialization gracefully
- With credentials: Verify initialization logs appear
- Cross-platform: Test on iOS, Android, and Web
Check the console for initialization logs:
[NewService] initialized
// or
[NewService] not configured - skipping
Troubleshooting
Service not initializing?
- Verify environment variable is set in
apps/app/.env
- Check variable is prefixed with
EXPO_PUBLIC_
- Restart Metro:
yarn app:start --clear
TypeScript errors?
- Ensure interface is exported from
types/index.ts
- Check import paths use
@/ alias
Platform-specific issues?
- Verify SDK is installed:
yarn add service-react-native
- Check SDK supports your React Native version
- Review SDK documentation for Expo compatibility