Desktop and cloud
Genie has one frontend and three ways to host it: the Electron desktop app with an embedded backend, the local web app (see Getting started), and a cloud web build on AWS. This page covers the desktop app, the cloud deployment and the local cloud simulator.
Team policy: test cloud changes against the local cloud simulator (genie-local-cloud) and update the CDK code without deploying it. Do not change the live AWS environment without explicit permission from the project owner. When new AWS resources are needed, write the AWS CLI or CDK commands into a file for review instead of running them.
Desktop app (Electron)
| Property | Value |
|---|---|
| Package | apps/desktop (genie-desktop) |
| Electron | ^35.0.0 |
| App id | com.geniegroup.genie.desktop |
| Product name | Genie |
| Entry | src/bootstrap.cjs, which imports src/main.js |
| Platform | macOS first (dmg and zip targets) |
Startup
bootstrap.cjsstarts the startup log, registers handlers for uncaught exceptions and unhandled rejections, and importsmain.js.main.jslooks for the Codex CLI (see below). If it is missing, the app logs that assistant features will be disabled and continues.- It starts the backend in the same process with
startLocalBackendfromgenie-backend:- host
127.0.0.1, port0(a random free port) - session data in
session-data/under Electron'suserDatafolder - the renderer build as the static root, so the backend serves the frontend
GENIE_CLOUD_API_URLpassed through if set
- host
- It opens the main window and loads
http://127.0.0.1:<port>. Because the page is served by the backend and runs inside Electron, the frontend uses same-origin/wsfor chat and the same backend for project data.
If the backend fails to start, the window shows a "Genie Desktop could not start" page with the error and a hint about GENIE_CODEX_COMMAND.
When you quit, the app closes the backend first, which ends every Codex app-server process. On macOS, closing the last window does not quit the app; activating it again reopens the window.
Window
| Setting | Value |
|---|---|
| Size | 1480 × 980, minimum 1100 × 760 |
contextIsolation | true |
sandbox | true |
nodeIntegration | false |
| Preload | src/preload.cjs |
Preload API
The preload script exposes window.genieDesktop:
| Member | Returns | Purpose |
|---|---|---|
target | "electron" | Lets the frontend detect the desktop app |
pickDirectories() | Promise<string[] | null> | Native "Choose folders to import" dialog (multiple folders). Used by the Choose folders… button in asset import. |
getAssistantRuntimeStatus() | Promise<{ codexCliInstalled, codexCliPath, codexCommand, message }> | Result of the Codex CLI lookup |
Codex CLI discovery
GENIE_CODEX_COMMAND. An absolute path must be an executable file; a bare command name is resolved withcommand -vin a loginzshshell. If the variable is set but nothing is found, discovery stops and the assistant is disabled with the message "GENIE_CODEX_COMMAND is set to …, but that executable was not found."/opt/homebrew/bin/codex/usr/local/bin/codexcommand -v codexin a loginzshshell (/bin/zsh -lic).
When the CLI is found, its absolute path is passed to the backend, which adds the CLI's folder to PATH for the app-server process.
Behaviour without the Codex CLI
The app opens normally with AI features disabled. Projects, the genome browser, assets and image export work. The chat shows "AI Features Unavailable" and "Install the Codex CLI to use assistant chats on this desktop app.", the composer placeholder reads "AI features unavailable", and a notice above the composer reads "Codex CLI is not installed. AI features are unavailable until you install the Codex CLI."
Packaging
| Command | Output |
|---|---|
pnpm desktop:dev | Builds the renderer, prepares resources and runs Electron from source |
pnpm desktop:pack | Same build, then electron-builder --dir: an unpacked Genie.app in apps/desktop/dist/ |
pnpm desktop:dist | Same build, then electron-builder --mac dmg zip in apps/desktop/dist/ |
Builds are unsigned (mac.identity is null and CSC_IDENTITY_AUTO_DISCOVERY=false), and the app is not packed into an asar archive. electron-builder's cache is kept in genie/.cache/electron-builder.
The packaged app carries two resource folders:
Folder in Genie.app/Contents/Resources/ | Source | Used as |
|---|---|---|
renderer/ | apps/standalone/.next-desktop (from pnpm build:desktop-renderer) | The backend's static root |
backend-resources/ | apps/desktop/.generated/backend-resources (from scripts/prepare-backend-resources.mjs) | The backend's resource root. Holds copies of the standard track registration sources from packages/genome-browser. |
When running from source (pnpm desktop:dev), the renderer root is apps/standalone/.next-desktop and the resource root is the genie/ folder.
Logs
Startup messages are appended to /tmp/genie-desktop-runtime.log, or to the file named by GENIE_DESKTOP_LOG_PATH: bootstrap start, the backend start options, the backend URL, startup errors, uncaught exceptions and unhandled rejections. Per-chat app-server logs are written to each session folder as app-server.log (see Backend reference).
Cloud deployment (AWS CDK)
The cloud web build is a static frontend with a serverless project API. It has no assistant. The CDK app is infra/aws-cdk (@genie/aws-cdk); the stack class is GenieCloudStack, deployed as GenieDevCloudStack by default.
Stack resources
| Resource | Details |
|---|---|
| Cognito user pool | <appName>-users, self sign-up, email sign-in, plus a web client. A pre-sign-up trigger (<appName>-pre-sign-up) confirms new accounts and marks their email verified, so no confirmation code is sent |
| DynamoDB table | On-demand single table (PK/SK with two global secondary indexes), streams enabled (new and old images), point-in-time recovery |
| S3 object bucket | Versioned, S3-managed encryption. Holds uploaded assets, plugin bundles, results and Studio outputs. |
| S3 static web bucket | Public access blocked, S3-managed encryption, SSL enforced |
| CloudFront distribution | S3 origin with origin access control; default root object index.html; 403 and 404 answered with /index.html (status 200) so client-side routes work; caching disabled; HTTPS redirect; price class 100 |
| Lambda function | Node.js 22 (lambda/api/index.mjs), with a log group kept for one week |
| API Gateway HTTP API | A default route that sends every request to the Lambda function |
| Bucket deployment | Uploads apps/standalone/dist (or the webDistPath context value) and invalidates /* |
| Custom resource | Writes genie-runtime-config.js to the web bucket |
Stack outputs: UserPoolId, UserPoolClientId, ProjectTableName, ObjectBucketName, StaticWebBucketName, StaticWebDistributionDomainName, StaticWebUrl and HttpApiUrl.
By default resources are retained when the stack is deleted (removalPolicy context retain).
Building and synthesizing
pnpm build runs vite build --mode web into apps/standalone/dist, and pnpm cdk:synth runs cdk synth in infra/aws-cdk:
pnpm build
pnpm cdk:synth
cdk:synth only renders the CloudFormation template into infra/aws-cdk/cdk.out; it does not change AWS. Following the team policy above, any deploy command goes into a file for review rather than being run directly.
Runtime configuration
The custom resource writes this file, so one frontend build can point at any stack:
window.__GENIE_RUNTIME_CONFIG__ = {
"appTarget": "web",
"backendMode": "cloud",
"assistantEnabled": "false",
"authRequired": "true",
"awsRegion": "<region>",
"cloudApiUrl": "<HTTP API URL>",
"userPoolId": "<user pool id>",
"userPoolClientId": "<client id>"
};
See Configuration.
No assistant in the cloud
The Lambda project API serves projects, assets, analysis state, Studio outputs, browser state, results and sharing. It does not run the Codex CLI. Its /models route answers 404 with "Assistant models are not available from the cloud project API." With assistantEnabled false, the frontend hides chat and mode-switch actions, analysis and plan routes redirect to Genome mode, and starting an assistant session fails with "Assistant sessions are only available in the desktop app."
Sign-in
When authRequired is true, visitors do not need an account. The first visit starts a guest session automatically: the API creates a Cognito user with a generated guest-…@guest.genie.invalid email and a random password (POST /auth/guest), and the browser keeps those guest credentials in local storage so the same guest returns on the next visit. Everything a guest creates is owned by that user's Cognito sub, exactly like a normal account.
The account button on the right of the header shows Guest with Create account…, or the account email once converted. The web app has no sign-in or sign-out, so a browser's work is never swapped out for another account's:
| Action | What happens |
|---|---|
| Create account… | POST /auth/convert-guest with the guest's token. The API checks that the email is free and the password meets the pool policy (8+ characters, upper and lower case, a number and a symbol), then sets the real email (marked verified, so no code is sent) and password on the same Cognito user. The sub does not change, so every guest project now belongs to the account. |
Guest work is protected in several ways:
- A saved guest is only replaced if Cognito reports it no longer exists; on any other error the app shows Try again, and replaced guest credentials are kept in a local archive (
genie.cloudGuestArchive.v1). - Guest creation is serialised across tabs with the Web Locks API, so two tabs opened at once share one guest.
- If a conversion fails half way, the API restores the guest email so the saved guest credentials keep working.
Sessions are renewed in the background with Cognito refresh tokens (POST /auth/refresh, valid for 10 years), and a request that comes back 401 renews the session once and retries. /auth/sign-in and /auth/sign-up are unchanged; sign-up refuses the reserved guest domain. Routes under /shared/ skip the session entirely so share links open for anyone.
Sharing
Sharing is implemented by the cloud project API (the Lambda function and genie-local-cloud); the local backend does not implement it.
| Route | Purpose |
|---|---|
GET /projects/:projectId/sharing | Read the owner's general-access setting |
PUT /projects/:projectId/sharing | Switch between restricted and anyone-with-link |
GET /shares/:shareId | Anonymous, view-only project payload |
| Shared asset, plugin and result routes | Expose only files referenced by an active share |
In the app, Share opens a dialog with General access ("Private" or "Anyone with the link"), the role "Viewer" and Copy link. The link opens /shared/:shareId, a read-only browser with a "View only" badge; changes made there are not saved. Share records live in the same DynamoDB table, and the grant can be revoked by switching back to "Private".
Local cloud simulator
genie-local-cloud is a separate Node server, checked out next to genie/ (it is not part of the monorepo). It implements the same project REST contract as the Lambda function so cloud mode can be developed without AWS.
| Property | Value |
|---|---|
| Start | pnpm dev:local-cloud from genie/ (or pnpm -C genie-local-cloud dev from the parent folder) |
| URL | http://localhost:8790 (PORT or GENIE_LOCAL_CLOUD_PORT; host HOST or GENIE_LOCAL_CLOUD_HOST) |
| Storage | .local-cloud/dynamodb/single-table.json (DynamoDB-shaped items with PK and SK) and .local-cloud/s3/objects/ (S3-style keys); override the root with GENIE_LOCAL_CLOUD_DATA_ROOT |
| Auth | Not verified; requests without a bearer token act as one local user |
| Scope | Project data and sharing only; no chat or model APIs |
| Latency | GENIE_LOCAL_CLOUD_LATENCY_MS adds a fixed delay to every non-preflight request; GENIE_LOCAL_CLOUD_LATENCY_JITTER_MS adds a random 0 to N ms on top |
pnpm dev:local-cloud:latency runs it with 800 ms latency and 400 ms jitter, and pnpm dev:web-cloud runs that together with pnpm dev:web. In web mode the Vite dev server proxies /api/cloud to http://localhost:8790, so the frontend uses /api/cloud as its project API.
The local backend can also mirror projects and browser state to a cloud project API, and read the browser state back before genome tools run, when it is started with GENIE_CLOUD_API_URL (for example http://localhost:8790 for the simulator).