This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange
What I Built
Mecropolis is a field and crop tracker for farms. A farm manager registers farms and fields, plans a season's crop, and logs observations, treatments and pest reports. The app works out where each crop is in its season from the weather that actually happened: growing degree days (GDD) summed from Open-Meteo temperatures since planting. Nobody moves a season from "planted" to "growing" by hand. When the GDD total crosses the crop's threshold, the season advances on its own.
Who it's for: a farm manager or agronomist who wants the season's story in one place, from the rain and heat that drove it to what was sprayed and when.
I built it with Western Cape, South Africa farms in mind, because that's where I grew up and it's what my current work is based around. The demo farms sit in the Swartland wheat belt. Nothing in the data model is tied to that region: farms, fields and coordinates are Sanity documents, and Open-Meteo returns weather for any point on earth. The same app works for any farm, in any region, at any time.
What you can do:
- Landing page (no sign-in): a regional weather map built on MapLibre, with every grid cell coloured by degree days accumulated this season. Live conditions, soil, pest pressure and regional yield statistics for the demo site sit alongside it.
- Dashboard: farms, fields, a season board, season comparison, CSV export, an activity feed and a recommendation queue (approve, reject, complete).
- Season page: a stage stepper, a GDD chart, a what-if scenario for rainfall and temperature, a field photo, the field's soil test PDF, dated notes with revision history and per-note restore, semantic search over the field log, and an AI summary of the season's stored records.
All data is real. Weather and soil come from live APIs, and treatments and yields are entered by the operator. Regional benchmarks are shown separately and are never used to fill in a field's yield. The crop model parameters are my own hand-written estimates. They have no cited source, and the app confirms it.
Live: https://mecropolis.pages.dev
Demo login:
- Username: demo
- Password: sanitydemo
The demo account is shared, so public writes (observations, treatments, recommendations) have global hourly caps.
Code
chantleyw
/
mecropolis
Field and crop tracker: Next.js, Sanity, Auth.js, Open-Meteo
Mecropolis
Field and crop tracker for Western Cape farms. Each season's crop stage comes from recorded weather: growing degree days (GDD) summed from Open-Meteo temperatures since planting. Regional yield statistics are listed separately with their source.
Live: https://mecropolis.pages.dev
Try it
-
/shows the regional weather map, live conditions, soil, pest and yield panels for the Swartland demo site. No sign-in needed. -
/loginsigns in with the demo account and opens/dashboard: farms, fields, the season board, compare, CSV export, the activity feed and the recommendation queue (approve, reject, complete). -
/seasons/:idshows one season: stage stepper, GDD chart, what-if scenario, field photo, a form to propose a recommendation with the season's evidence, the field's soil test PDF, dated notes (add, edit, delete) with revision history and per-note restore, and the field log (observations, treatments).
Anyone can read the dataset, so the sign-in only controls what the app shows. Every write…
Vite + React + TypeScript SPA, Cloudflare Pages Functions and Sanity. MIT licensed. The repo has 313 tests, CI runs typecheck, lint, tests, build and npm audit (kept at 0), and every commit is co-authored by Claude.
My Build Process
The tool
I used Claude Code for the whole build, from the first spec review to the final deploy. I started in the terminal with the Claude Code CLI, then moved to the Claude desktop app and used both side by side: the desktop app for longer sessions with the built-in browser preview, and the CLI in my terminal next to it. The work ran from 20 to 26 September across many sessions. I wrote a full project spec first (planning.txt in the repo), then asked Claude to review it for discrepancies and plan the build in phases. After that I worked mostly in short prompts: "start phase 1", "continue with the redesign", "is anything outstanding".
My first real prompt set the bar for the rest of the project:
Review the planning doc, normal and formatted versions, make sure there are no discrepancies and/or contradictions, then start with planning to start the build according to build spec.
Starting over halfway through
Mecropolis didn't end up on the architecture it started with. Version one was Next.js 16 with an embedded Sanity Studio and Auth.js credentials sign-in, deployed to Vercel. It worked locally, but the Studio rendering was slow and fragile, and the Next build got stuck on Vercel. Then Vercel refused a deploy over the commit-author email. I'd been fighting the hosting and the rendering engine more than building the app, so on 22 September I stopped and restarted:
Let's start over. What we need is a web app, not desktop app, everything else in the spec stands. So we could have React + Vite + TypeScript + React Router, Sanity backend content lake, documents, queries, mutations, all previous selected features + Sanity SDK with a similar type of UI, gradients, 3D progress, animations, no dependency on Sanity's rendering engine.
The rebuild is a Vite + React SPA on Cloudflare Pages. The browser reads a public-read Sanity dataset directly, and every write goes through a Pages Function that holds the write token. That split turned out to be the best decision of the project. It gave me one place to enforce sessions, origin checks, Zod validation and write caps, and it gave Sanity Functions a clean endpoint to call.
A day later I narrowed it further:
We do not implement the use of Sanity Studio because the rendering is causing a problem that will take extra time to debug. So we're building our own web app interface on top of the Sanity backend engine.
Reaching past the Studio: the App SDK
After the rebuild I tried the Sanity App SDK (@sanity/sdk-react) with AuthBoundary for sign-in. Two things went wrong:
- Claude's plan used
LoginCallbackanduseLoginUrls, and neither exists in the installed version. It checked the package's.d.tsfiles instead of trusting memory, found the real shape (AuthBoundarytakes component overrides, and the hook isuseLoginUrl, singular), and recorded the mismatch. - The SDK signs in with a Sanity account, and a judge or visitor won't have one on my project. I also watched the token vanish from localStorage after going back to the Sanity login.
I replaced the SDK sign-in with a signed session cookie issued by a Pages Function and kept Sanity as the content backend. The App SDK work was a detour, but it's the reason the final auth model is simple.
Prompts that worked
- Plain, concrete corrections. "The recent activity box on the dashboard should be bottom right, not lodged in between the farm names, it looks untidy" got a better result than any abstract design brief.
- Asking for real data sources before building. "How are we handling the weather forecasts and the harvest maps, are there APIs for this?" led to checking the FAO crop calendar and other crop calendar services for South African coverage. None covered it usefully, so the season stage is computed from degree days instead of looked up. That constraint shaped the whole app.
- Asking what Sanity could do for the app. I gave Claude a list of Sanity features and asked which would improve the app. That produced the second half of the build: Live Content API, Actions API drafts, Dataset Embeddings, Agent Actions, file assets, the History API and Sanity Functions.
- Standing rules. Early on I asked Claude to run a security review after every task. That's why the public writes have global caps counted in Sanity: the demo login is published, and an in-memory rate limiter only covers one isolate.
Prompts that didn't work
-The first UI looked like every other AI-generated dashboard. My honest feedback was "it works but it looks too basic". Asking for gradients, 3D and more animation made it busier, not better. What worked was the Impeccable design skill: it rolled several visual directions, I rejected the "Living Almanac" look and three others, and I pinned a Windy-style regional weather field with light panels. Then I asked Claude to "integrate an actual map", and the MapLibre map with OpenFreeMap tiles is now the centre of the landing page.
- Letting the redesign touch the logo. The redesign turned the seedling logo into four squares. I asked: "What happened to the original seedling logo, it looks like a Windows logo now". So now it's a seedling over a row of four grid cells, echoing the map.
Where the model got stuck, and how I course-corrected
-
Losing the plan after a context reset. After one long session was summarised, Claude started talking about Vercel again. My reply was "What are you talking about Vercel for, we agreed on Cloudflare Pages, decisions.md should have reflected this change". The fix was structural:
planning/next.mdnames the active plan and is updated at the end of every session, and the project instructions say to read it first. That stopped it happening again. -
Silent nulls from APIs. Open-Meteo's archive accepts
soil_temperature_0cmand returns all nulls, and the Climate API returns all-null evapotranspiration. Neither is an error, so the first versions showed empty charts. Each finding is now written down in the project rules, and the app usessoil_temperature_0_to_7cmand temperature plus rainfall only for projections. -
Documents nobody could see. The seed used dotted document IDs like
season.north-a.2026. In Sanity, dotted IDs are treated as private paths, so anonymous reads can't see them, even in a public dataset. The app loaded with empty lists and no error. Claude found the cause, exported a backup and re-keyed every document with-. -
Dead code that looked active. Seeded crop names such as "Wheat (SST 88)" never matched the crop model lookup, so GDD and the stage reconciler did nothing. Tests passed because they used clean names. The fix was an explicit
gddModelKeyfield on the crop document. -
Deploying Sanity Functions.
npx sanity blueprints deployfailed withEALLOWSCRIPTSbecause npx passed my npm config into the function build. Running the local CLI binary from the functions folder works.
Reaching into Workflows
The season lifecycle is the workflow: planning → planted → growing → pre-harvest → harvested → review. A reconciler (/api/advance) walks each season forward when its GDD total crosses the crop's threshold, and writes the stage change in a transaction guarded by ifRevisionId. Sanity Functions drive it:
- a document function runs on every new observation, treatment or weather snapshot
- a scheduled function runs at 02:00 UTC and walks every season
Both call the Pages Function with a bearer secret, so the reconcile logic lives in one place. Recommendations follow a review workflow with the Actions API: a proposal is saved as a draft, then published when approved or discarded when rejected.
Sanity Project Details
-
Project ID:
mns0vhec -
Dataset:
production(public read) - Try a query: seasons with crop and field
- Schemas: sanity/schemaTypes/
The content model: farm → field → season, with crop referenced by each season. observation, treatment, pestReport and weatherSnapshot reference a season. benchmark holds regional yield statistics, kept separate from field yields on purpose. agronomyRecommendation carries the evidence it was proposed from, and regionGrid stores the landing map's degree-day grid, accumulated incrementally so the map doesn't refetch 187 weather points on every visit.
Sanity features used:
| Feature | Used for |
|---|---|
| Custom schemas, references, nested objects, TypeGen | The farm, field, season and field-log model, with generated types |
GROQ with joins, references(), math::sum
|
Every read in the app |
| Live Content API | Views refresh when a document they read changes |
Transactions, ifRevisionId
|
Stage changes and note edits without lost updates |
| Actions API drafts | Proposed recommendations, published on approval |
| Dataset Embeddings | Semantic search over observations and treatments |
| Agent Actions (Prompt) | AI summary of a season's stored records |
| Assets API | Field photos (LQIP, palette, hotspot) and soil test PDFs |
| Portable Text and History API | Dated season notes with revision history and per-note restore |
| Sanity Functions and Blueprints | Stage reconcile on new records and nightly |

Top comments (1)
Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support