Ports Overview

What ports are

Ports are TypeScript interfaces that define what a native capability must provide. The SDK’s core logic depends only on these interfaces, never on specific implementations. Adapters live in src/adapters/ and satisfy the interfaces at runtime via defineConfig.

The deletion test

Remove any adapter directory under src/adapters/. The core still compiles. No adapter is on the import path of any component, hook, store, or form. This is the single mechanical invariant the architecture enforces.

All 16 ports

PortFilePurpose
BiometricProviderbiometrics.tsFingerprint / Face ID authentication
CameraProvidercamera.tsTake pictures via device camera
SecureStorageProviderstorage.tsEncrypted key-value storage
HapticsProviderhaptics.tsTactile feedback (impact, notification, selection)
NotificationProvidernotifications.tsLocal + push notifications, badge count
PurchaseProviderpurchases.tsIn-app purchases and subscriptions
PermissionsProviderpermissions.tsCheck and request OS permissions
ShareProvidershare.tsNative share sheet
ClipboardProviderclipboard.tsRead/write system clipboard
KeychainProviderkeychain.tsSecure credential storage (keychain / keystore)
LocationProviderlocation.tsGPS position, watch updates, foreground/background
MediaLibraryProvidermedia-library.tsPick images/videos, save to library, list albums
MapProvidermap.tsRender a native map with markers
WebBrowserProviderweb-browser.tsIn-app browser (SFSafariViewController / Chrome Custom Tabs)
ContactsProvidercontacts.tsRead device contacts, search, request permission
ReviewProviderreview.tsTrigger native app-review prompt

All 16 are defined in src/ports/ and re-exported from the main barrel as export type.

Interface definitions

BiometricProvider

interface BiometricProvider {
  authenticate(options?: {
    promptMessage?: string;
    cancelLabel?: string;
    fallbackLabel?: string;
  }): Promise<BiometricResult>;
  isAvailable(): Promise<BiometricAvailability>;
}

CameraProvider

interface CameraProvider {
  takePicture(options?: {
    quality?: number;
    base64?: boolean;
    facing?: 'front' | 'back';
  }): Promise<CameraResult>;
  requestPermission(): Promise<PermissionStatus>;
}

NotificationProvider

interface NotificationProvider {
  requestPermission(): Promise<PermissionStatus>;
  getToken(): Promise<string>;
  schedule(content: NotificationContent, options?: ScheduleOptions): Promise<string>;
  cancel(notificationId: string): Promise<void>;
  cancelAll(): Promise<void>;
  onReceived(handler: (n: NotificationContent) => void): () => void;
  onTapped(handler: (n: NotificationContent) => void): () => void;
  getBadgeCount(): Promise<number>;
  setBadgeCount(count: number): Promise<void>;
}

PurchaseProvider

interface PurchaseProvider {
  getProducts(ids: string[]): Promise<Product[]>;
  purchase(productId: string): Promise<PurchaseResult>;
  restore(): Promise<PurchaseResult[]>;
  getActiveSubscriptions(): Promise<PurchaseResult[]>;
  onPurchaseUpdate(handler: (result: PurchaseResult) => void): () => void;
}

Build your own adapter

Implement the port interface, then wire the result into defineConfig. No base class, no registration — the interface contract is the only requirement.

Example: custom CameraProvider using react-native-vision-camera

import type { CameraProvider, CameraResult, PermissionStatus } from '@objectifthunes/limestone-sdk';
import { Camera, useCameraDevice } from 'react-native-vision-camera';

export function createVisionCamera(): CameraProvider {
  return {
    async requestPermission(): Promise<PermissionStatus> {
      const status = await Camera.requestCameraPermission();
      return status === 'granted' ? 'granted' : 'denied';
    },

    async takePicture(options): Promise<CameraResult> {
      // Vision Camera requires a ref — wire this to your camera screen
      throw new Error('takePicture must be called from a mounted Camera component');
    },
  };
}

Wire it in limestone.config.ts

import { defineConfig } from '@objectifthunes/limestone-sdk';
import { createVisionCamera } from './adapters/vision-camera';

const config = defineConfig({
  theme: myTheme,
  api: {
    baseUrl: 'https://api.example.com',
    auth: {
      refreshEndpoint: '/auth/refresh',
      onUnauthorized: () => router.push('/login'),
    },
  },
  adapters: {
    camera: createVisionCamera(),
    // other adapters...
  },
});

The rest of the app — useCamera(), components, hooks — requires no changes. The core only sees the CameraProvider interface.