A marine life database and educational platform that combines my expertise as a PADI Divemaster and scuba diver with my skills as a full-stack developer. The platform features detailed species information, interactive data visualizations, and AI-powered content generation. Most of the underwater photography showcased on the site was captured during my own dives.
- My role
- Founder, architect and sole developer - web, iOS, Android, infrastructure
- Period
- 2023 - present

Project Overview
Red Sea Creatures is an educational resource for marine enthusiasts, researchers, and divers. It offers species taxonomy, habitat details, and visual insights. The site is designed to be user-friendly, fast, and scalable while showcasing real underwater photography. Beyond the web, the project extends to two native mobile clients built on the same API: an iPhone app written in Swift and SwiftUI, available on the App Store, and a content-identical Android app written in Kotlin and Jetpack Compose, available on Google Play. All three clients run against one self-hosted stack: a Docker Swarm service behind nginx and Cloudflare, with Redis, MySQL, and Manticore Search alongside it, deployed with zero-downtime rolling updates. The platform's most demanding feature is FishID: photo identification that matches a diver's photograph against 1,337 species from 10,646 Red Sea photographs, running as its own recognition service with a versioned dataset, a free allowance, and paid access sold through Stripe on the web and in-app purchases on both mobile platforms.
The project in numbers
- Built by
One person, end to end
Web platform, iOS app, Android app, a Python recognition service and the self-hosted infrastructure all three run on. No team, no agency, no contractor for the parts nobody enjoys.
- FishID recognition set
1,337 species from 10,646 photographs
Photo identification built as its own service, with the dataset shipped as a versioned artefact that updates in production without a deploy or a migration, and decoy classes so a photograph that is not a Red Sea fish is refused rather than misidentified.
- Mobile apps in production
iOS and Android, both live
Native Swift/SwiftUI and Kotlin/Jetpack Compose clients against the same API, both cleared store review - including an App Clip, home-screen widgets, an iMessage extension and a share extension that sends a photo straight to FishID.
- Deploys
Zero downtime, by image digest
A Docker Swarm stack with start-first, health-gated rolling updates, deployed by immutable digest after a tag-based deploy once rolled out nothing and left production on stale code.
- Search engine
Sphinx 2.2.11, from 2016
toManticore 29
Off an end-of-life engine without a week of regressions in the feature people use most. Proven with a differential harness that asked both engines the same 27 queries - ordinary words, Russian, real typos from the search log, operator-bearing strings and non-Latin canaries: 26 of 27 returned identical ids, and the one difference was an improvement, accent folding making a species reachable that had been unfindable to anyone typing plain ASCII.
- Page HTML, dive-site pages
632 KB
to188 KB
Then optimised what I had built: migrated to React Server Components with isolated client islands. Worst cases fell from 2.6 MB to 274 KB, cutting the RSC payload share of transferred bytes from 76-79% to 19-30%.
- Mobile Lighthouse, home
65
to93
Every major template improved: dive sites 71 -> 90, dive centres 78 -> 92.
- LCP on the worst pages
7.2s
to3.8s
Responsive WebP pipeline at 400/800/1100/1800px cut LCP image weight threefold and removed 136 competing preload requests on a single page.
- Blog CLS
0.247
to0
Layout shift eliminated outright, not merely reduced.
- TTFB
~1.3s
to~20ms
HTML and image delivery moved to a Cloudflare CDN with automatic cache invalidation on content change.
Challenges
- Managing a large dataset of marine species, their scientific classifications, and ecological information.
- Ensuring users can quickly search, filter, and explore species data efficiently.
- Hosting high-resolution images & interactive charts while keeping load times fast.
- Automating species descriptions, translations, and content updates dynamically.
- Serving high-quality images with optimized performance for global access.
- Keeping two native mobile clients in feature parity with each other and with the web, while still feeling native on each platform.
- Shipping code to a single production host without the 502s that a stop-then-start deploy causes.
- Keeping content fresh at the edge: a CDN cache that never expires serves stale species pages, one that expires too soon defeats the point.
- Absorbing traffic spikes and bot scans on a modest droplet without the database becoming the bottleneck.
- Getting off a search engine that had reached end of life - a 2016 Sphinx build with an npm client unmaintained for years - without a week of regressions in the one feature people use most.
- Identifying a fish from a diver's photograph - which in practice means a blurry, backlit, half-obscured frame, not a catalogue image.
- Answering honestly when the photograph is not a Red Sea fish at all, rather than confidently naming whichever species it resembles most.
- Charging for recognition across three separate tills - the website, the App Store and Google Play - without ending up with three different notions of who has access.
Solutions
- Developed a highly optimized, SEO-friendly front-end using Next.js & React.
- Implemented full-text search for fast, accurate species lookup, and later migrated it off the end-of-life Sphinx 2.2.11 onto Manticore Search 29, its maintained fork.
- Created interactive graphs and charts using Highcharts to display marine life insights.
- Integrated OpenAI API & Gemini API for AI-powered descriptions & translations.
- Hosted all high-resolution underwater images on AWS S3 + CloudFront for optimized delivery.
- Enhanced the media library with additional marine life photography using Flickr API.
- Gave hand-entered common names a source of their own, after discovering that the WoRMS and GBIF enrichment writers own their entries and delete them on every refresh - so a name typed in by hand survived only until the next sync.
- Ensured high availability & performance by hosting on DigitalOcean with a containerized Docker environment.
- Automated deployments and maintenance using GitHub Actions for CI/CD.
- Implemented error tracking and user analytics using Sentry & Google Analytics.
- Built a native Android client with Kotlin & Jetpack Compose (Material 3) as a content-identical port of the iOS app, sharing the same REST API and an Android-native navigation shell.
- Used Hilt for dependency injection and a Retrofit/OkHttp + kotlinx.serialization stack with a stale-while-revalidate Flow cache, so screens render from cache instantly and refresh in the background.
- Added Google Maps for the dive-site and dive-center maps, Jetpack Glance home-screen widgets, and Media3 (ExoPlayer) for background playback of the article podcasts.
- Wired Firebase Cloud Messaging for push notifications and Firebase Analytics with the same event names as iOS, plus HTTPS App Links so shared URLs open the matching native screen.
- Implemented social sign-in on Android (Google via Credential Manager, Facebook SDK, and Apple/Discord/X through PKCE in Chrome Custom Tabs) with tokens stored in Android Keystore-backed encrypted storage.
- Migrated production to a single-node Docker Swarm stack with start-first, health-gated rolling updates, so a release swaps containers in seconds with zero failed requests instead of taking the site down.
- Made the release pipeline deploy by immutable image digest rather than by tag, after a tag-based stack deploy silently rolled out nothing and left production serving stale code.
- Put Cloudflare in front of the origin for edge caching, TLS, and bot mitigation, with targeted per-URL purges fired from the app whenever content changes, so pages stay cached until they actually go stale.
- Layered caching end to end: Redis for taxonomy and query results, Next.js ISR for rendered pages, and the Cloudflare edge on top, with a single invalidation path that clears all three together.
- Added an /api/health probe that checks the database, Redis, and the search engine, used both as the Swarm health gate and by an automated post-deploy smoke test.
- Split that migration into two independently revertible steps by first rewriting the client onto SphinxQL over the MySQL wire protocol, which both engines speak - so the client swap could be proven against the old engine before the engine itself moved.
- Verified the swap with a differential harness that asked both engines the same 27 queries - ordinary words, Russian, real typos from the search log, strings carrying query operators, and non-Latin canaries - and diffed the returned ids per table.
- Stopped publishing the search engine's ports at the same time: Manticore has no authentication of its own, and the Sphinx it replaced had been exposed on 0.0.0.0. Everything that needs it reaches it over the Docker network.
- Made the deploy bootstrap the engine itself, after a first release rolled the app onto an empty search container: a fresh volume has no tables, and an index build cannot hot-rotate into nothing, so the pipeline now detects that case and builds before rolling out.
- Built a honeypot layer in middleware that fingerprints and traps scanners probing for PHP, ASP, and WordPress paths, plus Cloudflare Turnstile on public forms to block spam without a visible CAPTCHA.
- Documented the infrastructure as runbooks kept in the repo, covering the Swarm cutover, rollback path, and cron execution through the Swarm service.
- Built FishID, photo identification against the catalogue itself: a separate Python recognition service matching an uploaded photo to prototypes averaged from 10,646 photographs of 1,337 species, all of them taken in the Red Sea rather than pulled from stock aquarium imagery.
- Trained the set with deliberate decoy classes - cat, dog, camel - so a photograph that is not a Red Sea fish is answered as such instead of being forced into the nearest species.
- Shipped the recognition set as a versioned artefact that builds on publication, uploads to object storage and is picked up by production on its own, so improving identification needs neither a deploy nor a database migration.
- Made the recognition service non-critical to the site: its version and statistics are fetched with a short timeout and a five-minute cache, and a failure degrades one panel instead of taking down the page.
- Resolved FishID entitlements through a single function rather than a payment check, so admin grants, manual beta access, Stripe subscriptions, Apple in-app purchases, credit packs and the free monthly allowance all answer one question - does this account have access right now - and adding a fourth till changes nothing.
- Extended FishID to video on iOS: the app samples probe frames across a clip, splits it into scenes and picks the sharpest frame in each by variance of the Laplacian on a downscaled greyscale copy, tone-maps HDR footage to BT.709, and caches answers per frame - so a diver identifies a fish from a shaky clip without hunting for a still.
- Added a share extension on both platforms, so a photo or clip goes from the phone gallery straight to FishID and the answer appears over the gallery without the app opening at all.
- Shipped an App Clip, so a link to a species opens a working slice of the app for someone who has never installed it - with the affiliate banners stubbed out, because Apple forbids advertising in clips.
- Built an iMessage extension and sticker packs that search species, dive sites and centres and share a canonical link back into the conversation.
- Made the iOS app universal - iPhone, iPad and Apple silicon Mac - with measured widths driving column counts and a layout that switches between a tab bar and a persistent sidebar rather than stretching the phone UI.
- Gave the response and image caches a durable tier in Application Support, so dive sites, species and their photographs stay readable with no signal - which is the actual condition on a boat.
- Wired Handoff and universal links so a detail screen advertises its canonical web URL, a shared link opens the matching native screen, and the web pages emit Smart App Banner metadata pointing back.
- Added semantic search reaching the mobile clients, where the screen runs the query twice - an exact word match first for an instant list, then the semantic pass - so results appear immediately and improve a moment later.
- Built a per-city weather screen for divers: hourly temperature, wind, wave and tide charts sharing one cursor, 48 hours of history because two windy days ago decides today visibility, and sea state computed on the server so all three clients agree.
- Automated store releases against the App Store Connect and Google Play APIs - version creation, localised release notes, App Clip card, build attachment and submission - each step dry-run by default, refusing to run on a version mismatch or an already-uploaded build.
Key Features
- Comprehensive Marine Database - Detailed species profiles, taxonomy, and habitat information.
- Interactive Data Visualizations - Dynamic charts powered by Highcharts for marine life trends.
- Advanced Search - Full-text search on Manticore over SphinxQL, with accent folding, spell correction and modern rankers.
- AI-Powered Content - Automated species descriptions and translations via OpenAI API & Gemini API.
- Media Integration - Flickr API for additional image resources, AWS S3 + CloudFront for optimized photo delivery.
- Scalable Deployment - Hosted on DigitalOcean with Docker for efficient scaling.
- CI/CD & Monitoring - GitHub Actions for automated deployments, Sentry & Google Analytics for tracking performance.
- Native iOS App - A companion iPhone app built natively with Swift and SwiftUI, available on the App Store.
- Native Android App - A companion Android app built natively with Kotlin and Jetpack Compose, available on Google Play with the same screens, content, and design language as the iOS app.
- Home-Screen Widgets - Creature of the Hour and Sea Weather widgets on both platforms, built with WidgetKit on iOS and Jetpack Glance on Android.
- Push Notifications & Deep Links - Firebase Cloud Messaging alerts for new articles and marine events, with deep links and App Links that open the matching creature, dive site, or post natively.
- Offline-Friendly Mobile Reading - Stale-while-revalidate caching of API responses and images, so browsing stays fast and usable on a boat with poor connectivity.
- Zero-Downtime Deploys - Docker Swarm rolling updates with start-first ordering and health gating, deployed by immutable image digest.
- Multi-Layer Caching - Redis, Next.js ISR, and the Cloudflare edge behind one invalidation path, so a content edit propagates everywhere at once.
- Edge Delivery & Protection - Cloudflare CDN, TLS, and bot mitigation in front of the origin, with Turnstile guarding public forms.
- Health Checks & Smoke Tests - An /api/health probe covering MySQL, Redis, and the search engine, plus an automated post-deploy smoke run that reports to Telegram.
- Public REST API - OpenAPI/Swagger-documented endpoints shared by the web app and both mobile clients.
- FishID Photo Identification - Drop an underwater photo, including HEIC straight off an iPhone, and get a ranked shortlist of species with confidence figures, each linked to its catalogue card.
- Honest Refusals - Decoy classes in the recognition set mean a photograph that is not a Red Sea fish is rejected rather than misidentified.
- Recognition Billing Across Three Stores - A free monthly allowance, credit packs that never expire, and subscriptions, sold through Stripe on the web and in-app purchases on iOS and Android, all resolving to one account.
- Private by Default - Uploaded identification photos are never published on the site and never shown to other divers.
- FishID from Video - Scene detection and sharpest-frame selection pull an identifiable still out of a shaky dive clip.
- Share Extension - Send a photo or clip to FishID straight from the phone gallery; the answer appears without opening the app.
- App Clip - A species link opens a working slice of the app for someone who has never installed it.
- iMessage Extension & Stickers - Search species, dive sites and centres, and share a link without leaving the conversation.
- Universal iOS App - One app across iPhone, iPad and Apple silicon Mac, switching between a tab bar and a sidebar by measured width.
- Dive Weather - Per-city hourly charts for temperature, wind, wave and tide, plus the two days behind you that decide visibility.
- Semantic Search - Describe what you saw instead of naming it, with exact matches shown instantly and semantic results a moment later.
- Automated Store Releases - App Store Connect and Google Play API pipelines for versions, release notes, builds and submission, dry-run by default.
Project Info
Technologies
Frontend
Mobile
Backend
Search
Caching
Data Visualization
AI & APIs
Payments
Security
Hosting & Deployment
DevOps & CI/CD
Monitoring & Analytics
Need something like this?
Tell me what you are building and where it is stuck. I will tell you plainly whether it is work I should be doing.