npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

suzuki-vibe-boilerplate

v1.0.7

Published

Production-ready React Native boilerplate with Clerk, PostHog, RevenueCat, Sentry, and more

Readme

suzuki-vibe-boilerplate

A production-ready React Native starter kit built on Expo. Ships with authentication, analytics, crash reporting, in-app purchases, and push notifications — all wired up and ready to go.

Skip the week of SDK integration hell. Clone, configure your keys, and ship.


What's included

| Feature | SDK | |---|---| | Authentication | Clerk | | Analytics & Feature Flags | PostHog | | Crash Reporting | Sentry | | In-App Purchases | RevenueCat | | Push Notifications | Expo Notifications | | Navigation | Expo Router (file-based) | | Server State | TanStack Query | | UI State | Zustand | | Styling | NativeWind (Tailwind for RN) | | i18n | i18next | | Testing | Vitest |


Requirements

  • Node.js 18+
  • Expo CLInpm install -g expo-cli
  • EAS CLInpm install -g eas-cli (for builds and push notifications)
  • An iOS simulator or Android emulator (or a physical device)
  • Accounts at: Clerk, PostHog, Sentry, RevenueCat

Quick start

1. Clone and install

git clone https://github.com/sgoridotla1/suzuki-vibe-boilerplate.git my-app
cd my-app
npm install

2. Set your environment variables

cp .env.local.example .env.local

Open .env.local and fill in your API keys (see Environment variables below for where to find each one):

EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
EXPO_PUBLIC_POSTHOG_API_KEY=phc_...
EXPO_PUBLIC_REVENUECAT_IOS_KEY=appl_...
EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=goog_...
EXPO_PUBLIC_SENTRY_DSN=https://[email protected]/...
EXPO_PUBLIC_API_BASE_URL=http://localhost:3000

3. Rename the app

The boilerplate uses placeholder tokens throughout. Replace them with your actual app info:

# Replace __APP_NAME__ in app.json
# Replace __BUNDLE_ID__ with your bundle ID (e.g. com.yourcompany.myapp)
# Replace __BRAND_COLOR__ with your hex color (e.g. #6366f1)

4. Run it

npm start          # Start the Expo dev server
npm run ios        # Open on iOS simulator
npm run android    # Open on Android emulator

Environment variables

All environment variables live in config.ts — the single source of truth. You never read process.env anywhere else in the codebase.

Required

| Variable | Where to get it | |---|---| | EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY | Clerk Dashboard → Your app → API Keys | | EXPO_PUBLIC_POSTHOG_API_KEY | PostHog → Project Settings → Project API Key | | EXPO_PUBLIC_REVENUECAT_IOS_KEY | RevenueCat Dashboard → Project → Apps → iOS | | EXPO_PUBLIC_REVENUECAT_ANDROID_KEY | RevenueCat Dashboard → Project → Apps → Android | | EXPO_PUBLIC_SENTRY_DSN | Sentry → Project → Settings → Client Keys (DSN) | | EXPO_PUBLIC_API_BASE_URL | Your backend URL (no trailing slash) |

Optional

| Variable | Default | Description | |---|---|---| | EXPO_PUBLIC_POSTHOG_HOST | https://us.i.posthog.com | Change if self-hosting PostHog | | EXPO_PUBLIC_BRAND_COLOR | #6366f1 | Primary brand color used in Tailwind | | EXPO_PUBLIC_APP_SCHEME | Value from app.json | Deep link scheme (e.g. myapp://) | | SENTRY_AUTH_TOKEN | — | For source map uploads in EAS builds. Set via eas secret:create |

If a required variable is missing, the app throws a descriptive error at startup — you'll know exactly which key to add.


Project structure

├── app/                    # Screens (Expo Router file-based routing)
│   ├── _layout.tsx         # Root layout — provider composition and SDK init
│   ├── (auth)/             # Unauthenticated routes (sign-in, sign-up)
│   └── (app)/              # Authenticated routes (home, settings, paywall)
│
├── modules/                # Third-party SDK integrations
│   ├── auth/               # Clerk authentication
│   ├── api/                # Fetch client with auth headers
│   ├── analytics/          # PostHog events and feature flags
│   ├── monitoring/         # Sentry crash reporting
│   ├── paywall/            # RevenueCat in-app purchases
│   └── notifications/      # Expo push notifications
│
├── stores/                 # Zustand state
│   ├── user.store.ts       # User profile (userId, displayName, email)
│   └── app.store.ts        # App-level UI state (onboarding, color scheme, errors)
│
├── lib/                    # Shared utilities
│   ├── i18n.ts             # i18next setup and t() export
│   ├── query-client.ts     # TanStack Query shared client
│   └── use-color-scheme.ts # NativeWind-aware color scheme hook
│
├── locales/
│   └── en.json             # All user-facing strings
│
├── __tests__/              # Unit tests (Vitest)
├── assets/                 # App icons and splash screens
├── config.ts               # Environment variable config
└── tailwind.config.js      # NativeWind + brand color setup

How modules work

Each third-party SDK lives inside a modules/ folder. The rule is simple: nothing outside a module ever imports from that SDK directly.

// ✅ Correct
import { useAuth, signOut } from '@/modules/auth';

// ❌ Wrong — never import SDKs directly in screens or other modules
import { useUser } from '@clerk/clerk-expo';

This means:

  • Swapping an SDK only touches one module, not the whole app.
  • Tests mock at the module boundary, so tests never know which SDK is underneath.
  • Screens stay clean — no business logic, no SDK knowledge.

Screens

Screens in app/ are intentionally thin. They render layout, call hooks from modules and stores, and use t() for strings. That's it.

// app/(app)/settings.tsx
import { View } from 'react-native';
import { AuthGate } from '@/modules/auth';
import { useUserStore, selectDisplayName } from '@/stores/user.store';
import { t } from '@/lib/i18n';

export default function SettingsScreen() {
  const displayName = useUserStore(selectDisplayName);
  return (
    <AuthGate>
      <View className="flex-1 p-4">
        <Text>{t('settings.greeting', { name: displayName })}</Text>
      </View>
    </AuthGate>
  );
}

<AuthGate> redirects unauthenticated users to the sign-in screen automatically.


Authentication

Auth is handled by Clerk, wrapped in modules/auth.

import { useAuth, signOut, AuthGate } from '@/modules/auth';

// In a component
const { isSignedIn, userId } = useAuth();

// Outside a component (e.g. in a query function)
await signOut();

The AuthProvider is already set up in app/_layout.tsx. Sign-in and sign-up screens live at app/(auth)/sign-in.tsx and app/(auth)/sign-up.tsx.


API calls

Use the api client from modules/api. It automatically attaches the user's auth token to every request.

import { api } from '@/modules/api';

// In a TanStack Query hook
const { data } = useQuery({
  queryKey: ['profile'],
  queryFn: () => api.get<Profile>('/me'),
});

// POST
await api.post('/posts', { title: 'Hello world' });

Failed requests (non-2xx) throw an ApiError with .status and .body properties.


Analytics

import { track, identify, reset, useFeatureFlag } from '@/modules/analytics';

// Track an event
track('button_pressed', { screen: 'home', button: 'get_started' });

// Identify user after sign-in
identify(userId, { email, plan: 'free' });

// Feature flags
const isNewUiEnabled = useFeatureFlag('new-home-ui');

// Reset on sign-out
reset();

In-app purchases

import { usePremium, getOfferings, checkEntitlement } from '@/modules/paywall';

const { isPremium, activeEntitlements } = usePremium();

// Show the paywall screen
import { PaywallScreen } from '@/modules/paywall';
<PaywallScreen
  headline="Go Premium"
  features={['Unlimited access', 'No ads', 'Priority support']}
  ctaLabel="Start free trial"
  onPurchase={(pkg) => { /* handle purchase */ }}
  onRestore={() => { /* handle restore */ }}
/>

Crash reporting

import { captureError, captureMessage, setUser } from '@/modules/monitoring';

// Log an error
try {
  await riskyOperation();
} catch (err) {
  captureError(err, { screen: 'checkout' });
}

// Attach user context after sign-in
setUser({ id: userId, email });

// Clear on sign-out
setUser(null);

Push notifications

import { usePushNotifications, useRegisterPushTokenOnSignIn } from '@/modules/notifications';

// Get the device's push token
const { expoPushToken, isPermissionGranted } = usePushNotifications();

// Automatically register the token when the user signs in
useRegisterPushTokenOnSignIn(async (token) => {
  await api.post('/devices', { token });
});

State management

Zustand handles local UI and navigation state. TanStack Query handles server data.

// Local state — Zustand
import { useUserStore, selectDisplayName } from '@/stores/user.store';
const displayName = useUserStore(selectDisplayName);

// App state
import { useAppStore, selectColorScheme } from '@/stores/app.store';
const { showError } = useAppStore();
showError('Something went wrong');

// Server state — TanStack Query
import { useQuery } from '@tanstack/react-query';
import { api } from '@/modules/api';

const { data: profile } = useQuery({
  queryKey: ['profile'],
  queryFn: () => api.get<Profile>('/me'),
});

i18n

All user-facing strings must use t(). Add new keys to locales/en.json before using them.

import { t } from '@/lib/i18n';

// Simple key
t('common.loading')           // → "Loading..."

// With interpolation
t('settings.userId', { userId })   // → "Your ID: abc123"

Key naming convention: <screen>.<key> — e.g. settings.signOut, auth.signInTitle.


Styling

NativeWind (Tailwind CSS for React Native) is pre-configured.

<View className="flex-1 bg-white dark:bg-neutral-950 p-4">
  <Text className="text-xl font-bold text-brand">Hello</Text>
  <Button className="bg-brand rounded-lg px-4 py-2" />
</View>
  • Brand color: use bg-brand, text-brand, border-brand
  • Dark mode: prefix with dark: — it's automatic based on system setting
  • Color scheme hook: import useColorScheme from @/lib/use-color-scheme (not from react-native)

Testing

npm test           # Run all tests once
npm run test:watch # Watch mode
npm run tsc        # TypeScript type check
npm run lint       # ESLint
npm run format     # Prettier

Tests live in __tests__/ and use Vitest. Each module has its own test file. SDKs are mocked at the module boundary:

// __tests__/analytics.test.ts
vi.mock('posthog-react-native', () => ({
  default: vi.fn(() => mockClient),
}));

// Then test the module's public API, not the SDK internals

Pre-commit hooks (Husky + lint-staged) run ESLint and Prettier on staged files automatically.


Building for production

This project uses EAS Build.

# Configure EAS for your account
eas build:configure

# Build for iOS
eas build --platform ios --profile production

# Build for Android
eas build --platform android --profile production

# Submit to app stores
eas submit --platform ios
eas submit --platform android

For production, add your Sentry auth token as an EAS secret so source maps are uploaded automatically:

eas secret:create --scope project --name SENTRY_AUTH_TOKEN --value sntrys_...

Adding a new module

  1. Create modules/<name>/index.ts
  2. Add a JSDoc comment at the top naming the owned SDK and the boundary rule
  3. Export only what other parts of the app need — keep SDK types and internals private
  4. Create __tests__/<name>.test.ts and mock the SDK at the top with vi.mock()
  5. If the module needs a provider at startup, add it to app/_layout.tsx
// modules/storage/index.ts
/**
 * modules/storage — AsyncStorage wrapper.
 * All @react-native-async-storage/async-storage imports stay in this module.
 */
import AsyncStorage from '@react-native-async-storage/async-storage';

export async function getItem<T>(key: string): Promise<T | null> { ... }
export async function setItem<T>(key: string, value: T): Promise<void> { ... }

Adding a new screen

  1. Create the file under app/(app)/ (authenticated) or app/(auth)/ (unauthenticated)
  2. Wrap authenticated screens with <AuthGate>
  3. Add any new strings to locales/en.json first, then use t() in JSX
  4. Keep the file under ~60 lines — delegate logic to modules or stores
// app/(app)/profile.tsx
import { View, Text } from 'react-native';
import { AuthGate } from '@/modules/auth';
import { useUserStore, selectDisplayName } from '@/stores/user.store';
import { t } from '@/lib/i18n';

export default function ProfileScreen() {
  const displayName = useUserStore(selectDisplayName);
  return (
    <AuthGate>
      <View className="flex-1 p-4">
        <Text className="text-xl">{t('profile.greeting', { name: displayName })}</Text>
      </View>
    </AuthGate>
  );
}

Adding a new config key

  1. Add the variable to .env.local.example and .env.production.example
  2. Add it to config.ts — use assertEnv() for required keys, or a ?? 'default' expression for optional ones
  3. Add a JSDoc comment describing what the key does
// config.ts
/** Base URL of the image CDN. */
imageCdnUrl: process.env.EXPO_PUBLIC_IMAGE_CDN_URL ?? 'https://cdn.example.com',

Never read process.env anywhere except config.ts.


Provider order

Defined in app/_layout.tsx. Outer to inner:

AuthProvider (Clerk)
  → QueryClientProvider (TanStack Query)
    → AnalyticsProvider (PostHog)
      → Stack (Expo Router)

SDK initialization order at module load (before first render):

  1. initMonitoring() — Sentry, first so init-time crashes are captured
  2. initPaywall() — RevenueCat, configured per platform
  3. initAnalytics() — PostHog, client passed to AnalyticsProvider

License

MIT