[KongchangAI]
Tutorials· 5 min read· 2,530 words

Building an iNaturalist Data Display Tool with Claude Code on a Phone: The Complete Workflow

Building an iNaturalist Data Display Tool with Claude Code on a Phone: The Complete Workflow

Simon Willison built a zero-server iNaturalist data display tool using his phone and Claude Code

Simon Willison built a tool for displaying iNaturalist nature observation records entirely on his phone using Claude Code for web while camping. The project uses a three-layer serverless architecture: a Python CLI for spatiotemporal data aggregation, GitHub Actions for scheduled data scraping (Git Scraping pattern), and a purely static frontend page generated with a single precise prompt. The entire system has zero operational costs, leveraging CORS support for GitHub raw files as a free data API.

Project Background

Simon Willison (renowned developer and creator of Datasette), during a camping trip, built a tool for displaying iNaturalist nature observation records entirely using his phone and Claude Code for web. This project demonstrates the practical capabilities of AI-assisted programming on mobile devices, while also showcasing a lightweight yet complete data pipeline design approach.

Simon Willison is well-known in the developer community for democratizing data tools. His creation, Datasette, is an open-source tool for exploring and publishing data, with the core philosophy of transforming SQLite databases directly into interactive Web APIs and interfaces. Built on Python and SQLite, Datasette can convert any CSV, JSON, or even geospatial data files into web applications with SQL query interfaces and JSON APIs. Since SQLite is an embedded database that doesn't require a separate server process—the entire database is a single file—Datasette achieves "zero-configuration deployment." Simon has built a massive plugin ecosystem around Datasette (over 100 plugins), covering data visualization, geospatial display, full-text search, and more. He also created companion tools like sqlite-utils, forming a complete "SQLite ecosystem toolchain." This "data as product" mindset and pursuit of simplicity, portability, and transparency runs through many of his projects, and this iNaturalist tool is no exception.

iNaturalist is a global nature observation community platform jointly operated by the California Academy of Sciences and the National Geographic Society, with over 2 million registered users and 150 million observation records. Users can upload photos of plants and animals, and the platform provides initial species identification suggestions through computer vision models (deep learning-based image classification), which are then verified by community experts. iNaturalist offers a comprehensive RESTful API supporting multi-dimensional queries by user, geographic range, species taxonomy, and more, returning structured JSON data containing GPS coordinates, timestamps, taxonomic classification information, and multi-resolution image URLs. Specifically, its v1 API is built on Elasticsearch, supporting complex geospatial queries (such as bounding box and radius searches), returning up to 200 records per request with pagination cursor support. The taxonomic data in API responses follows standard biological classification hierarchy (Kingdom, Phylum, Class, Order, Family, Genus, Species), and each observation record includes a "Research Grade" designation—only species identifications confirmed by multiple community members receive this label. Notably, iNaturalist's data is also integrated into GBIF (Global Biodiversity Information Facility), making it one of the important data sources for global ecological research. It's this open API that provides the data foundation for Simon's project. Simon has two separate iNaturalist accounts and wanted to aggregate and display observation records from both accounts on a single page, organized by time and location.

Technical Architecture: Three-Layer Data Pipeline

The entire project consists of three components, forming a complete pipeline from data acquisition to frontend display.

Python CLI Tool: Data Acquisition and Aggregation

Simon first built a Python command-line tool called inaturalist-clumper for fetching observation data from the iNaturalist API and performing "clumping" processing. The default clumping rules are: observations within 2 hours of each other and within 5 kilometers are grouped together.

This spatiotemporal aggregation logic has a broad application foundation in Geographic Information Systems (GIS). From an algorithmic perspective, it's essentially a density-based clustering method, similar in concept to the classic DBSCAN algorithm—defining the scope of a "neighborhood" (here, a 2-hour time window and 5-kilometer spatial radius) to determine which data points belong to the same cluster. However, Simon's method has a key difference from standard DBSCAN: DBSCAN requires simultaneously meeting a density threshold (the minPts parameter, i.e., minimum number of points in a neighborhood), while Simon's approach is closer to single-linkage hierarchical clustering—a new record only needs to satisfy the spatiotemporal proximity condition with any record in an existing cluster to be included. This simplification is very effective in scenarios with small data volumes and sparse distributions, avoiding the complexity of DBSCAN's parameter tuning.

Geographic distance calculation typically uses the Haversine formula, which accounts for the Earth's spherical geometry and accurately calculates the great-circle distance between two points based on their latitude and longitude. Its core mathematical expression is d = 2r·arcsin(√(sin²(Δφ/2) + cos(φ₁)·cos(φ₂)·sin²(Δλ/2))), where r is the Earth's radius (approximately 6,371 km), φ is latitude, and λ is longitude. Simon's choice of a 5-kilometer threshold and 2-hour time window closely matches the spatiotemporal range of a typical hiking observation outing—this practice of encoding domain knowledge into algorithm parameters is a very important design decision in data engineering.

This aggregation logic is highly practical—it essentially represents the concept of "one observation outing," organizing scattered individual records into meaningful activity units.

Git Scraping Automation: Scheduled Data Updates with GitHub Actions

The second step is setting up a Git scraping repository (simonw/inaturalist-clumps) that periodically runs the CLI tool via GitHub Actions and saves the results as a clumps.json file.

Git Scraping is a technical pattern that Simon proposed and popularized in 2020. Its core idea is surprisingly simple: leverage GitHub Actions' scheduled trigger functionality (based on cron expression scheduling) to periodically run scripts that fetch external data, then commit and push the results to the repository via git commit and git push. The underlying mechanism of GitHub Actions is: each trigger causes GitHub to spin up a fresh virtual machine (runner) on Azure cloud infrastructure, providing a standard Linux environment with 2 CPU cores, 7GB of memory, and 14GB SSD storage. The minimum precision of cron scheduling is 5 minutes, but actual trigger times may have several minutes of delay, as GitHub queues scheduled tasks during high-load periods. For Git Scraping scenarios, a typical workflow YAML file requires only a dozen or so lines of configuration: defining trigger conditions (schedule + workflow_dispatch for manual triggering), setting up the Python environment, installing dependencies, running the script, and committing and pushing changes. GitHub Actions provides 2,000 minutes of runtime per month for each free account, more than sufficient for these lightweight data scraping tasks.

Each data update produces a new Git commit, meaning you not only have the latest data snapshot but automatically gain a complete history of data changes—when new observation records were added, when data was modified—all traceable through git log and git diff.

More critically, raw files on GitHub (accessed via the raw.githubusercontent.com domain) natively support CORS (Cross-Origin Resource Sharing) cross-domain access and can be directly fetched by frontend JavaScript. CORS is a browser security mechanism; by default, JavaScript on a webpage can only request resources from the same origin as the current page. To access data from other domains, the target server must explicitly declare permission for cross-origin access in HTTP response headers (via the Access-Control-Allow-Origin header). GitHub's raw file service sets Access-Control-Allow-Origin: * by default, meaning any webpage can directly fetch file contents from repositories via fetch() without needing an intermediary proxy server. This characteristic effectively makes GitHub repositories a free, version-controlled static data API.

Frontend Display Page: Development Completed with a Single Prompt

The final step is generating the frontend page. Simon used Claude Code for web—Anthropic's browser-based AI programming environment. Unlike the Claude Code CLI version that runs in a terminal, it operates entirely in the browser, where users describe requirements in natural language and the AI directly generates previewable web applications. The two forms have significant technical differences: Claude Code CLI can directly access the local file system, execute shell commands, and run test suites, making it suitable for complex multi-file project development and refactoring; while Claude Code for web runs in a browser sandbox, with generated code executing in an isolated WebContainer or iframe environment, supporting real-time preview but unable to access the local file system. The web version is better suited for rapid prototyping and standalone single-page application building, which perfectly matches Simon's phone-based use case. Both share the same underlying model capabilities, but their interaction paradigms and applicable scenarios differ significantly. This browser-based form means it doesn't depend on local development environment configuration and works equally well in mobile browsers—this is the technical prerequisite that enabled Simon to complete development on his phone during camping.

Simon issued a precise prompt to Claude Code, requesting an HTML application that:

  • Fetches JSON data from GitHub
  • Displays all observation records using iNaturalist's small.jpg thumbnails
  • Supports loading=lazy lazy loading
  • Shows large.jpg images in a modal when thumbnails are clicked
  • Also displays species common names

loading=lazy is a native lazy loading attribute in the HTML specification, first implemented in Chrome 76 in 2019 and subsequently supported by Firefox, Edge, and Safari. It works by having the browser use the Intersection Observer API to detect whether image elements are entering the area near the viewport, only initiating HTTP requests to download image resources when images are about to become visible. Compared to traditional JavaScript lazy loading libraries (like lazysizes), the native implementation offers zero JavaScript overhead and earlier loading timing decisions. For Simon's project, this attribute is crucial—iNaturalist observation records may contain hundreds of images, and loading them all simultaneously would not only consume massive bandwidth but severely impact page rendering performance on mobile devices.

A single prompt completed the entire frontend page development, demonstrating Claude Code's efficiency when handling well-defined requirements.

Design Highlights

Zero-Cost Serverless Architecture

The entire system requires no backend server to run. Data is updated periodically via GitHub Actions, static JSON files are hosted on GitHub, and the frontend is a purely static HTML/JS page. This means zero operational costs, zero hosting fees, and extremely high reliability.

This architectural philosophy is highly consistent with the Jamstack (JavaScript + API + Markup) approach that has gained popularity in recent years. Jamstack's core proposition is: shift dynamic functionality from runtime to build time, replacing traditional server-side rendering with pre-generated static resources and third-party API calls. The term was coined by Netlify CEO Mathias Biilmann in 2015, originally written as JAMstack. Early representative tools included static site generators like Jekyll and Hugo, later evolving into hybrid frameworks like Next.js and Nuxt.js that support Incremental Static Regeneration (ISR). Simon's project represents a minimalist implementation of Jamstack—it doesn't even need a static site generator. GitHub Actions directly serves as the build pipeline, the GitHub repository serves the dual role of static asset hosting and data API, and the frontend page is purely client-side rendered. This "frameworkless Jamstack" approach isn't suitable for large applications, but for small data display projects, it eliminates all unnecessary abstraction layers, bringing maintenance costs close to zero.

The reliability of this architecture stems from its simplicity—no risk of database connection pool exhaustion, no possibility of server process crashes, no operational burden of SSL certificate expiration. GitHub itself has an SLA (Service Level Agreement) of 99.9%, which for personal projects represents enterprise-grade availability.

For personal projects and small tools, this architectural pattern is worth considering—no database needed, no cloud server needed, just GitHub's free features to build a complete data application.

Complete Development Workflow on a Phone

Completing the entire project's development on a phone is noteworthy. Claude Code for web frees developers from desktop environments, enabling effective programming work on any device with a browser. For scenarios like camping, this flexibility is especially valuable.

This also signals that AI-assisted programming tools are redefining the "work environment"—as long as you can clearly describe your requirements, code generation no longer depends on specific IDEs or hardware configurations. From a broader perspective, this reflects a long-term trend in software development toolchains: moving from local to cloud-based. Early development required locally installed compilers and editors, then came cloud IDEs like GitHub Codespaces and Gitpod, and AI programming tools take it a step further—developers don't even need to understand the project's file structure and build process, just express intent in a conversational interface. This doesn't mean development knowledge becomes unimportant; quite the opposite—Simon was able to complete development with a single prompt precisely because he has deep understanding of the web technology stack and knows what to ask for and how to ask for it.

Precision in Prompt Engineering

Simon's prompt was only a single paragraph, but it contained complete technical specifications: data source URL, image sizing strategy, performance optimization requirements (lazy loading), interaction patterns (modal), and content requirements (species names). This precise requirement expression is key to efficiently using AI programming tools.

From a Prompt Engineering methodology perspective, Simon's prompt embodies several important principles. First is specificity—he didn't say "make a nice-looking page" but explicitly specified the data source address, specific image size variants (small.jpg and large.jpg), and interaction behavior. Second is technical anchoring—by mentioning specific HTML attributes like loading=lazy, he provided the AI with a clear technical implementation direction, preventing it from hesitating between multiple possible implementation approaches. Finally is complete context boundaries—the prompt implicitly conveys the constraint that "this is a pure frontend application," requiring no backend logic, with data already available in JSON format. In code generation scenarios, the most effective prompts are often not the longest ones, but those with the highest information density—every sentence narrows the AI's search space, guiding it toward the solution the developer already has in mind.

Takeaways for Developers

Although this project is modest in scale, it demonstrates a development paradigm worth learning from:

Toolchain decomposition thinking: Break complex requirements into independent, composable toolchains, keeping each layer simple and replaceable. Python CLI handles data logic, Git Scraping handles automated scheduling, and the frontend handles display—each with its own responsibility, loosely coupled and highly cohesive. This design philosophy has deep roots in the Unix programming tradition—"Do One Thing and Do It Well." Each component can be independently tested and replaced: if iNaturalist's API changes in the future, only the Python CLI needs modification; if you want to switch frontend frameworks, the data layer is completely unaffected; if you want to switch the data source from iNaturalist to eBird (another nature observation platform), you only need to replace the CLI tool while the rest of the pipeline remains unchanged.

Best use cases for AI programming: When developers have clear understanding of architecture and requirements, AI can greatly accelerate the implementation process, quickly transforming ideas into runnable code. The key isn't letting AI think for you, but whether you can clearly express your own thinking.

Broad applicability of Git Scraping: This pattern applies to any scenario requiring periodic external data fetching—monitoring API changes, tracking price fluctuations, recording public dataset updates—all can be automated at zero cost using the same approach. There are currently hundreds of public Git Scraping repositories on GitHub tracking various data, from California wildfire data to New York City restaurant inspection scores, from vaccination progress to electricity market prices. The appeal of this pattern lies in reducing "data monitoring"—a task that seemingly requires professional infrastructure—to a lightweight operation requiring nothing more than a YAML configuration file and a script.

Key Takeaways

  • Simon Willison built a complete data display tool entirely on his phone using Claude Code for web
  • The project uses a three-layer serverless architecture: Python CLI data processing + Git Scraping automation + pure static frontend
  • The aggregation algorithm groups observation records within 2 hours and 5 kilometers together, representing a single complete field observation outing
  • By leveraging CORS support for GitHub raw files, zero-cost data API hosting is achieved
  • A single precise prompt completed frontend page development including lazy loading, modals, and other features
#Claude Code#Git Scraping#iNaturalist#serverless architecture#AI-assisted programming#prompt engineering#Simon Willison
Share:

Related articles