Deployment
The repo has three parts that are tested and deployed independently: contracts, indexer, and web. Each part has its own CI job and its own deployment path.
CI
.github/workflows/ci.yml runs on every push and on workflow_dispatch. Each part is its own job so that when something goes red, the job name says immediately where to look.
| Job | Steps |
|---|---|
contracts | Checkout with submodules → Foundry toolchain → forge test -vv |
indexer | Node 24 → npm ci → npx tsc --noEmit → npm test |
web | Node 24 → npm ci → npx tsc --noEmit → npm test → npm run build |
The contracts job sets MONAD_TESTNET_RPC=https://testnet-rpc.monad.xyz because foundry.toml declares this variable; no test forks, but a missing variable is an unnecessary way to fail.
For web, the build is also a test: type errors or missing modules turn CI red instead of surfacing in front of someone opening the site.
CD — web
.github/workflows/deploy-web.yml runs on a push to main with changes in web/**, contracts/src/algorithms/**, infra/deploy-web.sh, infra/rewrite-profile.js, infra/deploy-web-function.sh, or the workflow itself.
The runner holds no credentials. It requests an OIDC token from GitHub, AWS exchanges the token for a short-lived session, and the role's trust policy names exactly this repo:
role-to-assume: arn:aws:iam::962761391641:role/mochi-deploy-web
S3_BUCKET: mochi-meme-web
CLOUDFRONT_DISTRIBUTION_ID: E1RY7HHFKRVFBJThe steps:
infra/deploy-web.sh— build the bundle, sync to S3, invalidate CloudFront.infra/deploy-web-function.sh— publish the CloudFront Functionmochi-profile-rewritewithGATE_SECRETtaken from GitHub secrets, so the secret does not sit in the repo. This step also invalidates the distribution.- Verify assets: read the list of
/_next/static/...inweb/out/index.htmlthencurleach one until all return 200.
Concurrency group deploy-web, no cancel-in-progress: a killed run can leave the bucket holding half of one bundle and half of another.
A 200 status proves nothing
S3 has no directory index, so a nonexistent path still returns 200 with the home page. The verification step therefore reads the served bytes, not the status code.
Docs on Cloudflare Pages
VitePress lives in web/docs and is not part of the web bundle. It is a separate build, hosted on Cloudflare Pages at docs.mochi.meme. Pages was chosen over CloudFront because the zone is already on Cloudflare: Pages issues a certificate for the custom domain automatically, so there is no ACM to create or CloudFront alias to add.
The Pages project is a direct upload type: Cloudflare's Git integration requires the Pages GitHub app on the repo, which this repo does not have, so .github/workflows/deploy-docs.yml builds and pushes the output with wrangler whenever a push to main changes web/docs/** (or web/package.json). The deploy step needs the CLOUDFLARE_API_TOKEN secret with Cloudflare Pages: Edit permission; without the secret the deploy step is skipped and the workflow stays green.
The old /docs URL on the app is permanently redirected to the new host by a CloudFront Function. See System architecture.
CD — indexer
.github/workflows/deploy-indexer.yml runs on a push to main with changes in indexer/** or infra/deploy-indexer.sh.
The VPS has no cloud API to authenticate against, so this is SSH rather than OIDC. The repo holds a dedicated deploy key in secrets (VPS_SSH_KEY, VPS_HOST, VPS_USER, VPS_PORT), created for exactly this workflow and added to the deploy user's authorized_keys. It is not the machine owner's personal key.
The infra/deploy-indexer.sh script:
rsyncthe source to the host..envandmochi.db*are excluded — the first holds the gate secret and the Pinata key, the latter is indexed state and takes hours to rebuild.npm ci --omit=devunder the host's Node 22 (/opt/mochi/node).- Verify
node_modules/.bin/tsxexists before restarting. sudo systemctl restart mochi-indexer.- Wait for
localhost:8787/healthto answer.
Concurrency group deploy-indexer, no cancel-in-progress: the script rsyncs before restarting, so two overlapping runs can leave the host carrying the source from one run and the process from another.
Manual commands
Contracts
Full deploy, including Identity and Community:
cd contracts
set -a && . ./.env && set +a
forge script script/Deploy.s.sol:Deploy \
--rpc-url "$MONAD_TESTNET_RPC" --private-key "$PRIVATE_KEY" --broadcastRedeploy only posts and the sorts, keeping handles and communities:
forge script script/RedeployPosts.s.sol:RedeployPosts \
--rpc-url "$MONAD_TESTNET_RPC" --private-key "$PRIVATE_KEY" --broadcastIndexer
./infra/deploy-indexer.shINDEXER_HOST defaults to ninjaverse-vps, INDEXER_DIR defaults to /opt/mochi/indexer.
Web
export S3_BUCKET=mochi-meme-web
export CLOUDFRONT_DISTRIBUTION_ID=E1RY7HHFKRVFBJ
./infra/deploy-web.shPublishing the edge function with the gate (if needed) lives in infra/deploy-web-function.sh; the GATE_SECRET variable is never written to any file in the repo.
Deployed contract addresses
Chain id 10143, last redeploy 2026-09-18, explorer https://testnet.monadscan.com.
| Contract | Address | Block |
|---|---|---|
| IdentityRegistry | 0xa67ef35974bc8874318d249b6e74c7bd5870d1db | 63285812 |
| CommunityRegistry | 0x41c19889a3218000482a1cd6e13640edcd16cf32 | 63285812 |
| PostRegistry | 0xaC74A6AF7Ec96e28F2885916673EF3B9aACF10F5 | 63379890 |
| ChronoFeed | 0xF9CC631A04C5398Be97d5410303Cd61bD56d1a79 | 63379890 |
| HotFeed | 0xAF50B9FE82E42a7fCb9B7a31BC1DDB9dd02a661d | 63379890 |
| BestFeed | 0x383C7Af1CA32f72f916a14bb03f95ec1D98A14E6 | 63379890 |
| ControversialFeed | 0x6A94E5a83102b04c5965F8A756e2cB84c6B7170c | 63379890 |
| AlgorithmRegistry | 0xeEC08F90CFe669e3366933cF1C72fe4f08A3F289 | 63379890 |
Identity and Community were deployed by script/Deploy.s.sol at block 63285812. The karma-aware half (posts and the sorts) was redeployed by script/RedeployPosts.s.sol at block 63379890, keeping Identity and Community unchanged. The default feed slot is HotFeed; BestFeed holds the second slot. Four algorithms are registered. No contract has a governance function. Full addresses and function references are in Contracts and addresses.
Never put secrets in the docs
This page contains no gate code, private key, or SSH key. Contract addresses are public data; the deployer's private key never sits in the repo.