DRTYLABS
WorkServicesBlogAboutContact
contact@drtylabs.ca
Multi-tenant SaaS · Web + API + Mobile

Safer Inventory

2026saferinventory.com ↗Software DevelopmentAPIMobile
// THE BRIEF

Safer Inventory is a Shopify + QuickBooks Online inventory operations platform — a real, shipping product with a marketing site, a web dashboard, and a native mobile app for warehouse staff. Multi-tenant from day one. Offline-first on mobile.

A pnpm + Turbo monorepo with three first-party apps and four shared packages. TypeScript end-to-end. The same design tokens render byte-for-byte identical surfaces across web and mobile.

3
First-Party Apps
4
Shared Packages
Multi-tenant
By Design
Offline-first
Mobile Sync
// THREE APPS, ONE MONOREPO

apps/web

Marketing site + dashboard

  • Next.js 16.2.4 on the App Router with React Server Components
  • Turbopack as the dev/build runtime — instant HMR, fast cold starts
  • Tailwind v4 with CSS custom-property tokens; zero CSS-in-JS runtime
  • Framer Motion for orchestrated reveals and spring micro-interactions
  • Next.js 16's new proxy.ts convention mounts clerkMiddleware at the edge
  • Auth-sensitive routes pinned to the Node runtime, not edge
  • beforeInteractive theme script to prevent flash of wrong theme

apps/mobile

Expo / React Native warehouse app

  • Expo SDK with expo-router (file-based, App Router parity)
  • expo-camera scanner: EAN-13, UPC, Code128, QR, PDF417, ITF14, Codabar
  • Local SQLite via Drizzle ORM caches catalog, inventory, and orders
  • pendingMutations queue drains writes when the device returns online
  • BullMQ-style sync pump exposes queued / syncing / conflict / failed counts
  • App-level lock — biometric or passcode after N minutes backgrounded
  • expo-haptics, expo-secure-store, expo-local-authentication baked in
  • Sentry @sentry/react-native at the root for crash + font-load capture

apps/api

Backend handlers + workers

  • Drizzle ORM end-to-end — same library on server and device, different drivers
  • Postgres for the server, SQLite on the device, identical type contracts
  • BullMQ workers backed by IORedis for queue processing
  • Upstash Redis REST for edge-friendly rate limiting and OAuth state storage
  • Per-organization rate limiting with an explicit owner-bypass for trusted operators
  • Stripe + RevenueCat reconciled into a unified subscriptionSource on the org
// FOUR SHARED PACKAGES

The discipline layer. Code reuse without the lock-in of a framework. Every package is plain TypeScript with explicit boundaries.

@safer/ui

Design system — React components shared between web and the mobile webview surfaces. Tokens defined in CSS custom properties; the mobile app re-implements the exact same token table in theme.ts because RN can't consume CSS, but the visual output is byte-for-byte aligned.

@safer/types

Cross-surface TypeScript contracts. A Shopify webhook payload type used by the API is the same type consumed by the web dashboard and the mobile app. End-to-end strict mode, zero any.

@safer/integrations

Shopify and QuickBooks Online OAuth adapters. Install flows, callback validation, refresh-token rotation, sandbox/production switching, scope negotiation, and webhook signature verification.

Internal config

ESLint, Prettier, TypeScript base config, Tailwind preset, and shared Drizzle config — the discipline layer that keeps three apps and four packages from drifting.

// AUTH, SESSIONS, AND OAUTH

Clerk handles identity. We handle the integration ceremony — OAuth state tokens stored in Redis with 10-minute TTLs, refresh-token rotation, and a branded loader overlay during the cross-domain handoff so users never see a blank tab.

  • Clerk v6 on both web (@clerk/nextjs) and mobile (@clerk/clerk-expo)
  • Email/password, Sign in with Apple, Google, GitHub OAuth
  • Custom JWT template (safer-api) with fallback to default token
  • Handles needs_client_trust terminal status alongside complete for device-trust flows
  • OAuth state tokens minted, stored in Redis with a 10-minute TTL, validated on callback — no in-flight CSRF window
  • AuthRequiredError boundary in install handlers redirects expired sessions to a graceful error UI instead of leaking a 500
// THE SAFER DESIGN SYSTEM

Aesthetic

Premium electrical contracting meets editorial magazine — deep navy #0c1e33 backgrounds, gold accents #d6b059 used sparingly, warm cream text #f0ede6 (never pure white).

Typography trio

Cormorant Garamond (serif display, italic for emphasis), Plus Jakarta Sans (geometric sans body), JetBrains Mono (numbers, SKUs, OAuth state IDs, technical metadata).

Surfaces

Glass-card with a hairline gold accent stripe across the top edge — suggests physical lighting on a real card. Multi-layer shadows mimic how real surfaces cast light.

Motion

Spring-based: damping 20, stiffness 180. Never bouncier (cheap), never stiffer (mechanical). One canonical config across web and mobile.

Theme

Dark by default, full light-mode override via html.light class with localStorage persistence and zero-flash hydration.

SAFER INVENTORYweb · api · mobile
// TECH STACK
Next.js 16.2.4React 19TypeScript StrictTailwind v4TurbopackExpo SDKReact NativeDrizzle ORMPostgreSQLSQLiteClerk v6BullMQRedis (Upstash)StripeRevenueCatSentryFramer Motionpnpm + TurborepoHusky + lint-staged

// Engineering practices

Server Components-first — auth gates resolve on the server, client bundles stay small. Multi-org tenancy baked into the core data model and the auth boundary, not bolted on. Root-cause-first debugging — the proxy.ts migration was diagnosed by reading the Next.js dist source for the PROXY_FILENAME constant rather than guessing.

The RouteLoaderProvider shows a Safer-branded full-screen overlay during OAuth redirects with animated marks, a gold key-flow line, cycling status pills, and a 30-second safety auto-dismiss. Most teams ship a blank tab during these transitions. We don't.