Skip to main content
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

FileWhat to Add
types/newservice.tsInterface definition
types/index.tsExport the new types
config/env.tsZod schema + isServiceConfigured
config/features.tsFeature flag
services/newservice.tsService implementation
app.tsxInitialization 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):
  1. Create new implementation using the same interface
  2. Update the export in the service file
  3. 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()

Platform-Specific SDKs

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

  1. Without credentials: Service should skip initialization gracefully
  2. With credentials: Verify initialization logs appear
  3. 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