Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

solidjs-crossplatform-starter

One codebase. Three platforms. Ship everywhere.

A production-ready starter for building web, mobile, and desktop apps
with SolidStart, Capacitor, and Tauri.

MIT License Stars Issues Pull Requests

SolidJS Tauri v2 Capacitor 8 TypeScript Tailwind CSS v4 ssgoi

Use this template

PrerequisitesGetting StartedBuild for Any PlatformExamplesContributing


Why this starter?

Most cross-platform solutions force you to pick one: Electron for desktop, React Native for mobile, or SSR for web. This starter gives you all three from a single SolidJS codebase:

  • Web — SSR with SolidStart, deployed as a Node server
  • Mobile — Native iOS & Android via Capacitor (real native APIs, not a webview wrapper)
  • Desktop — Lightweight native apps via Tauri v2 (~10MB binary, not 200MB Electron)

No code duplication. No framework lock-in. Just SolidJS everywhere.

The project runs on SolidStart (Vinxi + Vite) for file-based routing and SSR, styled with Tailwind CSS v4. Mobile targets iOS and Android via Capacitor 8 — real native APIs, not a webview wrapper. Desktop targets macOS, Linux, and Windows via Tauri v2, producing a ~10MB binary instead of Electron's 200MB. Page transitions are powered by ssgoi — iOS-style slide and Material fade+scale work out of the box. The whole thing is written in strict TypeScript, with ESLint, Prettier, and Vitest build tests preconfigured.

Prerequisites

Requirement Needed for Install
Node.js ≥ 18 All platforms nodejs.org
Rust + Cargo Desktop builds only rustup.rs
Xcode iOS builds only Mac App Store
Android Studio Android builds only developer.android.com

Just exploring? Web development works with Node.js alone — no Rust, Xcode, or Android Studio needed. Add them later when you're ready to target desktop or mobile.

Getting Started

# Clone and install
git clone https://github.com/llupRisinglll/solidjs-crossplatform-starter.git
cd solidjs-crossplatform-starter
npm install

# Start developing
npm run dev

Open http://localhost:3456 — you'll see a fully working demo app with interactive samples covering SolidJS reactivity patterns and a native features page that adapts to your current platform.

Platform Builds

Web

npm run build:web     # SSR build (Node server)
npm run preview       # Preview the production build

Mobile (iOS & Android)

First time? Generate the native projects:

npx cap add ios       # creates ios/ directory
npx cap add android   # creates android/ directory

Then build and open:

npm run build:mobile  # Static build + Capacitor sync
npm run cap:ios       # Open in Xcode
npm run cap:android   # Open in Android Studio

Desktop (macOS, Linux, Windows)

Desktop builds require system libraries. Install once:

Arch Linux
sudo pacman -S webkit2gtk-4.1
Ubuntu / Debian
sudo apt install libwebkit2gtk-4.1-dev build-essential libssl-dev libayatana-appindicator3-dev librsvg2-dev
Fedora
sudo dnf install webkit2gtk4.1-devel openssl-devel
macOS / Windows

See the Tauri prerequisites guide.

Then build:

npm run build:desktop   # Production binary + installer
npm run tauri:dev       # Dev with hot reload

How It Works

The same SolidJS code runs everywhere. The build system handles the differences:

Web Mobile Desktop
Rendering SSR (Node server) Client-side SPA Client-side SPA
Routing Standard Router HashRouter HashRouter
Native APIs Web APIs Capacitor plugins Tauri plugins
Output .output/server/ .output/public/ Binary + installer

Platform Build Modes

Web uses SSR (server-side rendering) with Router for full SEO and streaming. Native platforms (desktop, mobile) use static SPA mode with HashRouter because there's no server to render HTML — Tauri and Capacitor load files directly from disk.

This means server functions, streaming, and other SSR features are web-only. Code that runs on native platforms must work entirely on the client. The build system handles this automatically via the PLATFORM env var.

Backend Integration

Most apps talk to a backend. The starter provides:

  • Vite proxy — Uncomment the proxy config in app.config.ts to forward /api calls to your backend during development (avoids CORS)
  • src/lib/api.ts — Platform-aware API base URL resolution. Web uses relative paths (works with proxy in dev, same-origin in prod). Desktop defaults to http://localhost:4000 for a sidecar backend. Set VITE_API_URL in .env to override.
  • src/lib/auth.ts — Token management and fetchWithAuth() wrapper that attaches Bearer tokens and redirects on 401

Auth Guard

The starter includes a provider-agnostic auth pattern:

// Wrap individual components
<AuthGuard fallback={<p>Redirecting...</p>}>
  <ProtectedContent />
</AuthGuard>

// Or use route groups — see src/routes/(protected)/
// All routes in (protected)/ are automatically guarded

Replace isAuthenticated() in src/lib/auth.ts with your auth provider's check (Supabase, Better Auth, Firebase, etc.).

Dark Mode & Theming

The starter respects prefers-color-scheme out of the box. All sample components use Tailwind's dark: variants. For manual control, use the ThemeToggle component:

import ThemeToggle from "~/components/ThemeToggle";
// renders a system/light/dark cycle button

Platform Detection

import { detectPlatform, isMobile, isDesktop } from "~/lib/platform";

// Runtime — adapts UI at runtime
const platform = detectPlatform(); // "web" | "mobile" | "desktop"

// Build-time — tree-shakes platform-specific code
if (import.meta.env.IS_DESKTOP) {
  // Only included in desktop builds
}

Native Features

Use dynamic imports so all platforms compile cleanly:

async function triggerHaptic() {
  if (detectPlatform() === "mobile") {
    const { Haptics, ImpactStyle } = await import("@capacitor/haptics");
    await Haptics.impact({ style: ImpactStyle.Medium });
  } else if (detectPlatform() === "desktop") {
    const { message } = await import("@tauri-apps/plugin-dialog");
    await message("Hello from Tauri!", { title: "Native Dialog" });
  }
}

Transitions

Pages transition automatically based on the detected design language:

  • iOS — Slide push/pop (swipe-back gesture from left edge)
  • Material — Fade + scale

Customize in src/assets/css/transitions.css.

Examples

The starter includes interactive samples that compile on all platforms. Run the app and visit /samples, or read the source in src/routes/samples/.

Sample Route Concepts
Counter /samples/counter createSignal, createMemo, createEffect, onCleanup
Todo List /samples/todos createStore, <For>, <Show>, event handling
Data Fetching /samples/fetch createResource, <Suspense>, <ErrorBoundary>
Form Handling /samples/forms Controlled inputs, validation, derived state
Store & Produce /samples/store createStore, produce, nested state, derived
Server-Sent Events /samples/sse createSSE, real-time updates, auto-reconnect
Counter — Signals & Reactivity
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);

createEffect(() => {
  if (!autoIncrement()) return;
  const id = setInterval(() => setCount((c) => c + 1), 1000);
  onCleanup(() => clearInterval(id));
});
Todo List — Stores & Fine-Grained Updates
const [todos, setTodos] = createStore<Todo[]>([]);

// Surgically update one property on one item — no re-render of the list
setTodos(
  (t) => t.id === id,
  "done",
  (done) => !done,
);
Data Fetching — Resources & Suspense
const [users, { refetch }] = createResource(enabled, fetchUsers);

<ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}>
  <Suspense fallback={<Spinner />}>
    <For each={users()}>{(user) => <UserCard user={user} />}</For>
  </Suspense>
</ErrorBoundary>;
Form Handling — Validation as Derived State
const [email, setEmail] = createSignal("");
const emailError = createMemo(() =>
  email().length > 0 && !email().includes("@") ? "Invalid email" : "",
);
const isValid = createMemo(() => email().includes("@") && !emailError());

Scripts

Command Description
npm run dev Start web dev server on port 3456
npm run dev:desktop Start desktop dev server on port 3457 (SSR disabled)
npm run dev:all Run web + desktop dev servers simultaneously
npm run build:web Build for web (SSR)
npm run build:mobile Build for mobile (static + Capacitor)
npm run build:desktop Build for desktop (static + Tauri)
npm run tauri:dev Desktop dev server with hot reload
npm run cap:ios Open iOS project in Xcode
npm run cap:android Open Android project in Android Studio
npm run lint Run ESLint
npm run format Run Prettier
npm run test:unit Component tests (solid-testing-library)
npm run test:build E2E build tests for all platforms
npm run test:e2e Playwright E2E tests

Project Structure

solidjs-crossplatform-starter/
├── src/
│   ├── routes/                  # File-based routing
│   │   ├── index.tsx            # Home page
│   │   ├── native.tsx           # Native features showcase
│   │   ├── (protected).tsx      # Auth-guarded route group layout
│   │   ├── (protected)/
│   │   │   └── dashboard.tsx    # Example protected route → /dashboard
│   │   ├── demo/
│   │   │   ├── index.tsx        # Transitions demo
│   │   │   └── detail.tsx       # Detail page (push transition)
│   │   ├── docs/
│   │   │   └── [[lang]].tsx     # Optional param → /docs, /docs/en
│   │   ├── files/
│   │   │   └── [...path].tsx    # Catch-all param → /files/any/path
│   │   └── samples/
│   │       ├── index.tsx        # Samples index with inline demo
│   │       ├── counter.tsx      # Signals, memos, effects
│   │       ├── todos.tsx        # Stores, For, Show
│   │       ├── fetch.tsx        # createResource, Suspense
│   │       ├── forms.tsx        # Inputs, validation
│   │       ├── store.tsx        # createStore, produce, nested state
│   │       └── sse.tsx          # Server-Sent Events demo
│   ├── components/
│   │   ├── AuthGuard.tsx        # Route guard (redirect if unauthenticated)
│   │   └── ThemeToggle.tsx      # Dark/light/system theme switcher
│   ├── lib/
│   │   ├── platform.ts          # Runtime platform detection
│   │   ├── api.ts               # Platform-aware API base URL + fetch
│   │   ├── auth.ts              # Token management + fetchWithAuth
│   │   ├── sse.ts               # SSE client with auto-reconnect
│   │   ├── transitions.ts       # Transition direction & CSS
│   │   └── swipe-back.ts        # iOS-style swipe gesture
│   ├── assets/css/
│   │   ├── app.css              # Tailwind, dark mode, scrollbars
│   │   └── transitions.css      # iOS slide / Material fade+scale
│   ├── app.tsx                  # Root layout with router + transitions
│   ├── entry-client.tsx         # Client entry point
│   └── entry-server.tsx         # Server entry point
├── src-tauri/                   # Tauri config + Rust backend
├── tests/
│   ├── build/                   # Build verification tests
│   └── unit/                    # Component tests (solid-testing-library)
├── e2e/                         # Playwright E2E tests
├── .github/workflows/ci.yml    # CI: lint, test, E2E
├── docs/                        # Architecture decisions & guides
├── app.config.ts                # SolidStart + Vite + devtools config
├── capacitor.config.ts          # Capacitor config
├── platform.config.ts           # Enable/disable platforms
├── eslint.config.js             # ESLint config
└── .prettierrc                  # Prettier config

Making It Your Own

Everything in src/routes/samples/ and src/routes/demo/ is demo content — safe to delete once you start building your app.

1. Choose your platforms

Edit platform.config.ts to disable platforms you don't need:

export const platformConfig = {
  web: true,
  mobile: false, // disables Capacitor
  desktop: true,
};

This prevents unused native dependencies from being bundled into other platform builds.

2. Add your own routes

Drop .tsx files into src/routes/ — SolidStart picks them up automatically via file-based routing:

src/routes/
├── index.tsx        # → /
├── about.tsx        # → /about
└── dashboard/
    └── index.tsx    # → /dashboard

Route precedence: Static routes always take priority over dynamic [param] routes. If you have both src/routes/admin.tsx and src/routes/[slug].tsx, visiting /admin will always render the static route. No need for a reserved-routes.ts guard.

3. Call native APIs safely

Use dynamic imports so all platforms compile cleanly, regardless of which native SDKs are installed:

const platform = detectPlatform(); // "web" | "mobile" | "desktop"

if (platform === "mobile") {
  const { Camera } = await import("@capacitor/camera");
  // iOS/Android only
} else if (platform === "desktop") {
  const { open } = await import("@tauri-apps/plugin-dialog");
  // macOS/Linux/Windows only
}

4. AI-assisted development

This starter ships with CLAUDE.md and llms.txt preconfigured for AI coding assistants (Claude Code, Cursor, Copilot, etc.). Edit CLAUDE.md to add your project-specific rules, conventions, and gotchas — your AI assistant will follow them automatically.

CLAUDE.md    — project rules, commands, patterns, gotchas
llms.txt     — file index so AI tools understand the codebase structure

Contributing

Contributions are welcome! Please read the Contributing Guide before submitting a pull request.

This project follows the Contributor Covenant Code of Conduct.

License

MIT

About

LLM-ready Cross-platform starter for web, mobile, and desktop — SolidStart + Capacitor + Tauri. One codebase, three platforms.

Topics

Resources

Code of conduct

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Contributors

Languages