Run Midnight on your machine
The fastest way to connect a Lovable app to Midnight is to run a local standalone stack: node + indexer + proof-server in Docker on localhost. Your app runs in the browser, Lace points at ws://localhost:9944, and every SDK version is guaranteed to match the chain. No faucet, no DUST sync stalls, no Preprod quirks.
bun scripts/midnight-standalone.mjs up
Requires Docker Desktop (macOS/Windows) or Docker Engine (Linux). First run pulls ~1 GB and takes 2–5 minutes; later boots are seconds.
Do not use :latest — the midnight-node latest tag frequently 404s, and mismatched versions cause ZKIR /check 400 errors. This triple is the current local-dev combination from the Midnight Support Matrix.
- 1
Docker running
Open Docker Desktop and wait for the whale icon, or on Linux runsudo systemctl start docker. Then confirmdocker infoworks. - 2
Start the local stack
In your project root run:bun scripts/midnight-standalone.mjs up
The script writes.midnight/standalone.docker-compose.yml, pulls the pinned images, boots the three containers, and polls until the endpoints are ready. - 3
Verify in the browser
Open /undeployed-preflight in your Lovable app. Four green pills means the app can reach the local stack. If any pill is red, the exact endpoint and error are shown. - 4
Point Lace at localhost
In Lace: Settings → Network → Custom → RPC =ws://localhost:9944. Lace will label the network "Preview" — that's cosmetic; the address prefixmn_addr_undeployed1…is the truth. No tNIGHT → tDUST dance here — that trap is Preview / Preprod only. The genesis wallet is pre-funded with unlimited tDUST. - 5
Deploy the contract
VITE_NETWORK_ID=undeployed bun scripts/deploy-midnight.mjs
The deploy script builds a headless wallet from the genesis seed0x000…0002directly — Lace isn't required for the deploy itself. Paste the printed hex address intoVITE_DEFAULT_CONTRACTin your Lovable secrets. Grab the lovable-midnight skill → to bake all nine deploy-script rules into your own Lovable account. - 6
Wire the wallet UI (optional)
For the Connect-Lace button, RPC-mode toggle, and shielded/unshielded address readout, copy the boilerplate from /wallet. It handles the DApp Connector v4 handshake and enumerates wallets byapiVersion.
VITE_NETWORK_ID=undeployed VITE_INDEXER_URL=http://localhost:8088/api/v4/graphql VITE_INDEXER_WS_URL=ws://localhost:8088/api/v4/graphql/ws VITE_NODE_RPC=ws://localhost:9944 VITE_PROOF_SERVER_URL=http://localhost:6300
Skip Docker with Effectstream
effectstream/effectstream is a community chain-abstraction orchestrator that wraps the Midnight node, indexer, and proof server as plain npm binaries — no Docker Desktop, no WSL, no BIOS virtualization. One bunx command supervises the whole dev stack (node + indexer + proof server + your deploy step + frontend) with status and logssubcommands. If Docker is fighting you on Windows, this is the fastest path back to building.
1 · Install the binaries
bun add -d @effectstream/midnight-node \ @effectstream/midnight-indexer \ @effectstream/midnight-proof-server \ @effectstream/orchestrator
Same binaries the Midnight team ships — just resolved through npm instead of Docker Hub.
2 · Run the dev loop
bunx orchestrator start --background bunx orchestrator status bunx orchestrator logs bunx orchestrator stop
Once running, point VITE_INDEXER_URL, VITE_PROOF_SERVER_URL, and node RPC at the local ports the orchestrator prints on start.
Reference templates worth reading
- templates/evm-midnight-v2 — EVM + Midnight, ERC-721 sync, ZK contracts, full React frontend. Closest match to a hackathon dApp that spans two chains.
- templates/zswap-da — Midnight Zswap for decentralized liquidity. Good reference for anyone building token-flow demos on Undeployed.
Community project — not an official Midnight release. If a binary version drifts from the support matrix, fall back to the Docker Compose recipe above.
Host the same stack on Fly.io
Local Docker is perfect for solo dev. But when a judge or teammate needs to hit your dApp with Lace from their own laptop, you need the Undeployed stack on public infra. Here's the recipe distilled from the Tokenized Choreo Kits build — including the blocker that's still open.
choreo-node— Midnight standalone node, 6PN-internal only, reached asws://choreo-node.internal:9944. No public HTTP.choreo-indexer— public HTTPS + WSS on/api/v4/graphql.choreo-proof— public HTTPS proof server.choreo-faucet— public HTTPS/grantendpoint, in-memory rate limit, pre-funded once from the genesis seed.
Bootstrap shape: scripts/fly-bootstrap.sh creates all four apps + a 1 GB volume for the node, sets FAUCET_SEED, and deploys.
- Proof server needs
memory = "2gb"— the k=13 proving key OOMs on 1 GB mid-mint. auto_stop_machines = falseon proof + node. Cold start is ~4 min of user-visible "Proving…".- Proof-server binary is IPv4-only. Fine as-is via the public
https://…fly.devURL (fly-proxy enters over IPv4). If you ever need 6PN access, add asocatsidecar binding[::]→127.0.0.1. - Node RPC must bind IPv6 or the indexer/faucet can't reach it via
.internal:[processes] app = "--experimental-rpc-endpoint \"listen-addr=[::]:9944,methods=unsafe\""
- Never scale
choreo-nodeabove 1 machine — two machines = two participants = forked chain. Runflyctl scale count 1after every deploy. - No
[http_service]on the node — expose port 9944 via[[services]]only, so it stays 6PN-internal. - Pin
midnight-node:0.22.5. Do not bump to 2.x — those are Partner Chain builds that need Cardanodb-syncand crash-loop on standalone. - Persistent
chain_datavolume (1 GB) on the node — wipe it and every previously-deployed contract address becomes invalid. - Faucet wallet has to be pre-funded once from the genesis seed
…0002before/grantworks. Hit/healthto see the balance while it syncs.
On Fly, midnight-node:0.22.5 with CFG_PRESET=dev + SIDECHAIN_BLOCK_BENEFICIARY=<hex> boots in partner-chain mode, not standalone sealer mode. Logs show Idle (0 peers) forever and one line: Failed to trigger bootstrap: No known peers.
Downstream effect: the faucet wallet (buildFromSeed) never finishes sync, getUnshieldedAddress() returns null, and /grant returns 503 faucet warming up. Proof and indexer are healthy in this state (/version → 8.0.3; GraphQL { __typename } responds on /api/v4/graphql) — the blocker is block authoring, not plumbing.
Working theory: SIDECHAIN_BLOCK_BENEFICIARY alone flips the image into partner-chain expectations. A standalone --dev sealer needs a different env combination, or the entrypoint expects a flag that [processes] in fly/node/fly.toml is currently overriding.
Next probe: flyctl ssh console -a choreo-node, dump /entrypoint.sh and the image's supported env vars, then diff against midnight-local-dev/standalone.yml, which authors blocks fine locally with the same tag.
Workaround while unresolved: use the local Docker stack above for Undeployed. Fly hosting only unlocks once the standalone sealer boots.
scripts/fly-bootstrap.sh— creates the 4 apps + volume + secrets, deploys all four.- Fund the faucet once by sending tDUST from the genesis deployer wallet (seed
…0002) to the address shown by/health. scripts/fly-deploy-contract.shruns on a Fly Machine (so it reacheschoreo-node.internalover 6PN) and prints the contract address.- Paste that hex into your app's
VITE_DEFAULT_CONTRACTenv var and republish.
Local Docker for offline dev, preflight, and iteration. Fly when you need a publicly demoable dApp that any judge with Lace can hit from their own laptop.
⚠ Fly is public infra. Treat FAUCET_SEED like a real key, keep the /grant rate limit on, and never point Lace at the hosted node from a Mainnet account.