An AI-powered desktop coding editor that scaffolds React Native projects using natural language prompts. Built with Tauri, React, FastAPI, and PydanticAI.
- Part 1 — Scaffolding Agent + Expo Snack Preview ✅
- Part 2 — Editor Agent ✅
- Part 3 — Collaboration with YJS ✅
- Part 4 — Release Build & Distribution ✅
| Layer | Technology |
|---|---|
| Desktop | Tauri 2.0 + Rust |
| Frontend | React + TypeScript |
| Backend | FastAPI + Python |
| AI | PydanticAI via OpenRouter |
| Preview | Expo Snack |
| Collaboration | Node.js + YJS |
git clone https://github.com/bjoxiah/coding-editor.git
cd coding-editorcd app
pnpm install
pnpm tauri devcd backendCreate a .env file inside the backend folder:
OPEN_ROUTER_API_KEY=your-openrouter-api-keyGet your API key at openrouter.ai
Then start the server:
poetry install
poetry run serveBoth the desktop app and backend must be running simultaneously. Once up, open the desktop app, configure the API URL in settings, and start scaffolding.
cd socketCreate a .env file inside the socket folder:
PORT=your-portThen start the server:
pnpm devUpdate the WebSocket URL in the app by clicking the gear icon on the Editor page.
If you want to run the React frontend without the Rust backend:
cd app && pnpm dev --port 1421coding-editor/
├── app/ # Tauri + React desktop app
│ ├── src/ # React source
│ └── src-tauri/ # Rust backend + Tauri config
│ ├── tauri.conf.json # App config, CSP, updater
│ ├── icons/ # Auto-generated app icons
│ └── Cargo.toml # Rust dependencies
├── backend/ # FastAPI + PydanticAI agent
├── socket/ # Node.js YJS collaboration server
└── .github/
└── workflows/
└── release.yml # CI/CD release pipeline
React (webview)
│
├── invoke() ──→ Rust ──→ FastAPI backend (not subject to CSP)
│
└── new WebSocket() ──→ YJS socket server (must be whitelisted in CSP)
All FastAPI calls are routed through Rust commands — the webview never calls FastAPI directly. Only the YJS WebSocket connection is made directly from React, so only that URL needs to be in the CSP.
This section documents how to build and ship a signed, auto-updating release using GitHub Actions.
The backend/ and socket/ folders are server-side services — they are not bundled into the Tauri binary and deploy separately. The GitHub Actions release workflow only builds the app/ folder.
Each release is triggered by pushing a git tag. The workflow builds the app across all platforms in parallel, signs the macOS binary, and uploads all artifacts plus a latest.json updater manifest to a GitHub Release.
I get my source icon image from https://icon.kitchen which usually comes as a .icns image then I convert to .png by running this:
Note:
sipsis a macOS-only command. On Windows or Linux use cloudconvert.com to convert your source image to PNG first.
sips -s format png ~/Desktop/your-icon.icns --out ~/Desktop/source.pngI copy the image to the icons folder inside src-tauri and then run this from inside the app/ folder:
pnpm tauri icon src-tauri/icons/source.pngThis auto-generates all required icon sizes into app/src-tauri/icons/. No manual changes to tauri.conf.json are needed — the config already points to these paths by default.
The CSP (Content Security Policy) controls what network connections the Tauri webview is allowed to make. Since all FastAPI calls go through Rust, only the YJS WebSocket URL and the images url need to be listed.
In app/src-tauri/tauri.conf.json:
"app": {
"security": {
"csp": "default-src 'self'; connect-src 'self' ws://localhost:1234 https://*.amazonaws.com; img-src 'self' blob: data: https://*.amazonaws.com https://api.qrserver.com; style-src 'self' 'unsafe-inline'"
}
}Replace 1234 with the port your YJS socket server runs on.
When you deploy the socket server, update this to use your real domain:
"connect-src 'self' wss://socket.yourapp.com"Note:
ws://(local) becomeswss://(production) once your server is behind HTTPS.
Run this once from inside the app/ folder:
pnpm tauri signer generate -w ~/.tauri/release.keySet a password when prompted and write it down. This creates two files:
~/.tauri/release.key— private key (never commit this)~/.tauri/release.key.pub— public key
Get the public key:
cat ~/.tauri/release.key.pub | pbcopyAdd the updater config to app/src-tauri/tauri.conf.json:
"plugins": {
"updater": {
"pubkey": "PASTE_PUBLIC_KEY_HERE",
"endpoints": [
"https://github.com/YOUR_USERNAME/YOUR_REPO/releases/latest/download/latest.json"
]
}
}Replace YOUR_USERNAME and YOUR_REPO with your actual GitHub username and repo name.
From inside the app/ folder:
pnpm tauri add updaterThis automatically updates Cargo.toml and registers the plugin. Confirm app/src-tauri/Cargo.toml now contains:
tauri-plugin-updater = "2"And that your lib.rs registers it:
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())Without signing, macOS Gatekeeper blocks your app entirely on other machines. Notarized apps open without any warnings.
- Go to developer.apple.com/account → Certificates, Identifiers & Profiles → Certificates → click +
- Select Developer ID Application → click Continue
- Before uploading, generate a CSR on your Mac:
- Open Keychain Access → top menu → Certificate Assistant → Request a Certificate from a Certificate Authority
- Fill in your email and name, select Saved to disk, click Continue
- Save the
.certSigningRequestfile to your Desktop
- Back in the browser, select G2 Sub-CA (Xcode 11.4.1 or later), upload the CSR file, click Continue
- Download the resulting
developerID_application.cerand double-click it to install into Keychain
- Open Keychain Access → My Certificates
- Find Developer ID Application: Your Name (TEAMID)
- Expand it — confirm a private key is nested underneath
- Right-click the certificate → Export → save as
certificate.p12on your Desktop - Set a strong password when prompted — this becomes
APPLE_CERTIFICATE_PASSWORD
base64 -i ~/Desktop/certificate.p12 | pbcopy- Go to appleid.apple.com → Sign-In and Security → App-Specific Passwords → Generate
- Name it
tauri-cior whatever makes sense to you → copy thexxxx-xxxx-xxxx-xxxxpassword
Go to your repo → Settings → Secrets and variables → Actions → New repository secret.
Add all of the following:
| Secret | Value | Where to get it |
|---|---|---|
APPLE_CERTIFICATE |
base64 encoded .p12 | Step 4c above |
APPLE_CERTIFICATE_PASSWORD |
.p12 export password | Step 4b above |
APPLE_ID |
your Apple developer email | your Apple account |
APPLE_PASSWORD |
app-specific password | Step 4d above |
APPLE_TEAM_ID |
10-char ID e.g. AB12CD34EF |
developer.apple.com/account → Membership |
TAURI_SIGNING_PRIVATE_KEY |
contents of ~/.tauri/release.key |
run cat ~/.tauri/release.key |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
password set in Step 3a | what you wrote down |
GITHUB_TOKENdoes not need to be added — it is provided automatically by GitHub Actions on every run.
Create the file .github/workflows/release.yml in your repo:
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest
args: '--target aarch64-apple-darwin'
- platform: macos-latest
args: '--target x86_64-apple-darwin'
- platform: ubuntu-22.04
args: ''
- platform: windows-latest
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Install Linux dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf
- name: Install frontend dependencies
working-directory: app
run: pnpm install
- name: Build and release
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
projectPath: app
tagName: ${{ github.ref_name }}
releaseName: 'v__VERSION__'
releaseBody: 'See changelog for details.'
releaseDraft: true
prerelease: true
args: ${{ matrix.args }}The version in app/src-tauri/tauri.conf.json is the source of truth. To avoid updating it manually, point it at your package.json:
{
"version": "../package.json"
}Then bump with:
cd app
npm version patch # 1.0.0 → 1.0.1Commit everything and push, then tag:
git add .
git commit -m "chore: configure production release"
git push origin main
git tag v1.0.0-beta.1
git push origin v1.0.0-beta.1The tag push triggers the workflow. Go to the Actions tab to watch it run across all 4 platform runners in parallel (~15 minutes). When complete, a draft release appears in Releases with all artifacts attached:
YourApp_1.0.0_aarch64.dmg ← macOS Apple Silicon
YourApp_1.0.0_x64.dmg ← macOS Intel
YourApp_1.0.0_x64-setup.exe ← Windows
YourApp_1.0.0_amd64.deb ← Linux Debian
YourApp_1.0.0_amd64.AppImage ← Linux universal
latest.json ← auto-updater manifest
Review then click Publish release.
git tag -d v1.0.0-beta.1 # delete locally
git push origin --delete v1.0.0-beta.1 # delete from GitHubThen retag and push again. Only do this with beta/prerelease tags — never delete a real release tag.
| Symptom | Likely cause | Fix |
|---|---|---|
| macOS runner fails on signing | APPLE_CERTIFICATE is malformed |
Re-run base64 -i certificate.p12 and re-paste the secret |
| Notarization timeout | Apple servers are slow | Re-run the failed job |
| App blocked by Gatekeeper on install | Not signed or not notarized | Ensure all 5 Apple secrets are set correctly |
| YJS connection refused in app | URL not in CSP | Add the WebSocket URL to connect-src in tauri.conf.json |
latest.json missing from release |
Updater plugin not configured | Confirm pnpm tauri add updater was run and pubkey is set |
This project is under active development. Feel free to open issues or PRs.
⭐ If you find this useful, star the repo and subscribe for the rest of the series!