Install
$ agentstack add skill-leo-atienza-atlas-claude-crossplatform-monorepo ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Cross-Platform Monorepo
When to Use This Skill
Auto-activate when: building an app that targets 2+ platforms (mobile + web, mobile + desktop, or all three), setting up a monorepo with Turborepo, or sharing code between Expo/Tauri/Next.js projects. SK-058 (Universal Conductor) routes here.
What Can Be Shared
| Layer | Mobile (Expo) | Desktop (Tauri) | Web (Next.js) | Shareable | |-------|--------------|-----------------|---------------|-----------| | Types/interfaces | Yes | Yes | Yes | 100% | | Business logic | Yes | Yes | Yes | 100% | | API client | Yes | Yes | Yes | 100% | | State stores | Yes | Yes | Yes | 95% (persister differs) | | Validation | Yes | Yes | Yes | 100% | | UI components | RN components | React components | React components | ~70% (web↔desktop share, mobile diverges) | | Navigation | Expo Router | React Router | Next.js Router | 0% (platform-specific) | | Storage | MMKV / SQLite | tauri-plugin-store / SQL | localStorage / IndexedDB | 0% (use shared interface) | | Native APIs | Expo modules | Tauri commands | Web APIs | 0% (use shared interface) |
Monorepo Structure
my-app/
├── apps/
│ ├── mobile/ # Expo app
│ │ ├── app/ # Expo Router pages
│ │ ├── app.json
│ │ └── metro.config.js
│ ├── desktop/ # Tauri app
│ │ ├── src/ # React frontend
│ │ ├── src-tauri/ # Rust backend
│ │ └── tauri.conf.json
│ └── web/ # Next.js app
│ ├── app/ # App Router pages
│ └── next.config.ts
├── packages/
│ ├── shared/ # Business logic, types, validation
│ │ ├── src/
│ │ │ ├── types/
│ │ │ ├── utils/
│ │ │ ├── validation/
│ │ │ └── constants/
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── api-client/ # Shared API/Supabase client
│ │ ├── src/
│ │ │ ├── client.ts
│ │ │ ├── queries/
│ │ │ └── mutations/
│ │ └── package.json
│ ├── store/ # Shared state (Zustand)
│ │ ├── src/
│ │ │ ├── auth-store.ts
│ │ │ ├── app-store.ts
│ │ │ └── types.ts
│ │ └── package.json
│ └── ui/ # Shared React components (web + desktop)
│ ├── src/
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── index.ts
│ └── package.json
├── tooling/
│ ├── typescript/ # Shared tsconfig
│ │ └── base.json
│ └── eslint/ # Shared ESLint config
│ └── base.js
├── turbo.json
├── package.json
└── .github/workflows/
└── ci.yml
Turborepo Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "src-tauri/target/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
},
"typecheck": {
"dependsOn": ["^build"]
}
}
}
Root package.json:
{
"private": true,
"workspaces": ["apps/*", "packages/*", "tooling/*"],
"scripts": {
"dev": "turbo dev",
"dev:mobile": "turbo dev --filter=mobile",
"dev:desktop": "turbo dev --filter=desktop",
"dev:web": "turbo dev --filter=web",
"build": "turbo build",
"lint": "turbo lint",
"test": "turbo test",
"typecheck": "turbo typecheck"
},
"devDependencies": {
"turbo": "^2.4"
},
"packageManager": "pnpm@9.15.0"
}
Platform-Specific Implementations
The .native.tsx / .web.tsx Pattern
Metro (Expo) and webpack/Vite (web/desktop) resolve platform extensions automatically:
packages/store/src/
persist.ts # Shared interface
persist.native.ts # MMKV implementation (mobile)
persist.web.ts # localStorage implementation (web + desktop)
persist.tauri.ts # tauri-plugin-store implementation (desktop)
Shared interface:
// packages/store/src/persist.ts
export interface StoragePersister {
getItem(key: string): Promise;
setItem(key: string, value: string): Promise;
removeItem(key: string): Promise;
}
Mobile (MMKV):
// packages/store/src/persist.native.ts
import { MMKV } from 'react-native-mmkv';
import type { StoragePersister } from './persist';
const storage = new MMKV();
export const persister: StoragePersister = {
getItem: async (key) => storage.getString(key) ?? null,
setItem: async (key, value) => storage.set(key, value),
removeItem: async (key) => storage.delete(key),
};
Web:
// packages/store/src/persist.web.ts
import type { StoragePersister } from './persist';
export const persister: StoragePersister = {
getItem: async (key) => localStorage.getItem(key),
setItem: async (key, value) => localStorage.setItem(key, value),
removeItem: async (key) => localStorage.removeItem(key),
};
Desktop (Tauri):
// packages/store/src/persist.tauri.ts
import { LazyStore } from '@tauri-apps/plugin-store';
import type { StoragePersister } from './persist';
const store = new LazyStore('app-data.json');
export const persister: StoragePersister = {
getItem: async (key) => (await store.get(key)) ?? null,
setItem: async (key, value) => { await store.set(key, value); await store.save(); },
removeItem: async (key) => { await store.delete(key); await store.save(); },
};
Shared State Store
// packages/store/src/auth-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import type { StoragePersister } from './persist';
interface AuthState {
user: { id: string; email: string } | null;
token: string | null;
setAuth: (user: AuthState['user'], token: string) => void;
clearAuth: () => void;
}
export function createAuthStore(persister: StoragePersister) {
return create()(
persist(
(set) => ({
user: null,
token: null,
setAuth: (user, token) => set({ user, token }),
clearAuth: () => set({ user: null, token: null }),
}),
{
name: 'auth',
storage: createJSONStorage(() => persister),
}
)
);
}
Usage in each app:
// apps/mobile/stores/auth.ts
import { createAuthStore } from '@myapp/store/auth-store';
import { persister } from '@myapp/store/persist'; // resolves to .native.ts
export const useAuthStore = createAuthStore(persister);
// apps/web/stores/auth.ts
import { createAuthStore } from '@myapp/store/auth-store';
import { persister } from '@myapp/store/persist'; // resolves to .web.ts
export const useAuthStore = createAuthStore(persister);
Shared API Client
// packages/api-client/src/client.ts
import { createClient } from '@supabase/supabase-js';
import type { Database } from '@myapp/shared/types/database';
export function createApiClient(supabaseUrl: string, supabaseKey: string) {
return createClient(supabaseUrl, supabaseKey);
}
// packages/api-client/src/queries/tasks.ts
import type { SupabaseClient } from '@supabase/supabase-js';
export async function getTasks(client: SupabaseClient, userId: string) {
const { data, error } = await client
.from('tasks')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
}
export async function createTask(client: SupabaseClient, task: { title: string; user_id: string }) {
const { data, error } = await client.from('tasks').insert(task).select().single();
if (error) throw error;
return data;
}
Metro Config (Expo)
// apps/mobile/metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
const projectRoot = __dirname;
const monorepoRoot = path.resolve(projectRoot, '../..');
const config = getDefaultConfig(projectRoot);
// Watch all files in the monorepo
config.watchFolders = [monorepoRoot];
// Resolve packages from monorepo root
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
];
// Ensure only one copy of react
config.resolver.disableHierarchicalLookup = true;
module.exports = config;
TypeScript Path Setup
// tooling/typescript/base.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true
}
}
// packages/shared/tsconfig.json
{
"extends": "../../tooling/typescript/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
CI/CD Matrix
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
shared:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm turbo typecheck lint test --filter='./packages/*'
mobile:
needs: shared
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build --filter=mobile
desktop:
needs: shared
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- uses: dtolnay/rust-toolchain@stable
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build --filter=desktop
web:
needs: shared
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build --filter=web
Build & Deploy Commands
| Platform | Build | Deploy | |----------|-------|--------| | Mobile | eas build --platform all | eas submit | | Desktop | pnpm tauri build | GitHub Releases + auto-updater | | Web | next build | vercel deploy or vercel --prod |
Adding a New Shared Package
mkdir -p packages/new-package/src
cd packages/new-package
# package.json
cat > package.json << 'EOF'
{
"name": "@myapp/new-package",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src/"
}
}
EOF
# Then in any app:
# pnpm add @myapp/new-package --workspace --filter mobile
Integration
- SK-058 — Universal Conductor routes multi-platform tasks here
- SK-088 — Tauri Desktop Engine for the
apps/desktop/target - SK-090 — Local-First Architecture shared across platforms via persister pattern
- SK-091 — Edge Intelligence with platform-specific model loaders
- SK-089 — Device Hardware Bridge for mobile-specific hardware access
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Leo-Atienza
- Source: Leo-Atienza/atlas-claude
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.