Penspace
AI-powered book summaries & reading-habit platform
Penspace is a production reading platform where users discover books through a Pinterest-style feed, read AI-generated chapter summaries in six languages, capture physical-book highlights with camera OCR, and build daily reading streaks. It spans a Flutter app, a Node.js API, a React admin CMS, and AWS infrastructure, all built and operated by one engineer, live at penspace.in.
The Problem
Reading apps either dump long summaries on users or gamify without substance. Penspace needed to make book content genuinely consumable (multilingual, visual, habit-forming) while staying operable by a solo engineer: content ingestion, translation, media processing and push infrastructure all had to run themselves.
The Solution
A three-part system: a feature-first Flutter app (Riverpod 3, go_router, Dio with retry/auth/language interceptors) for discovery, reading, OCR highlights and streaks; a layered Express/MongoDB API where every content model carries a translations array selected by Accept-Language; and an admin CMS with one-click AI summary generation. Translation is fully asynchronous: an event queue fans each summary out to OpenAI for five languages with timeouts, failure marking and retry-on-boot.
Architecture
- Flutter app → feature-first modules (feed, reader, streaks, highlights, library) over a strict features → shared → core layering with Riverpod 3 Notifiers
- Firebase Auth (Google / Apple / email) bridged to backend JWTs: the API verifies Firebase ID tokens, links accounts by verified email, and re-issues its own JWT so all services stay Firebase-agnostic
- Express API: routes → middleware → controllers → services → Mongoose models; 12 route domains + 70-endpoint admin surface
- Async pipeline: EventEmitter → p-queue → OpenAI translation into 5 languages, with 5-minute timeout races and boot-time retry of pending jobs
- Media: S3 uploads → SQS → Thumbor CDN variants, plus sharp-generated WebP covers; ffmpeg video thumbnails
- Streaks: timezone-correct dayKey protocol with a validity window covering all timezones; node-cron + FCM for reminder pushes
- Infra: AWS EC2 (Mumbai) behind nginx with PM2, MongoDB Atlas, SES for OTP email, Universal/App Links served from Express
Challenges & How I Solved Them
One identity across Firebase and a legacy JWT backend
Social logins had to coexist with existing email accounts without takeover risk. The bridge verifies Firebase tokens server-side, only links by email when Firebase reports it verified, atomically upserts users, and caps live JWTs at five per user.
Streaks that survive timezones
A streak that resets because the server lives in UTC kills the habit loop. Clients send a local dayKey; the server accepts it only within the window of all possible timezones, then computes current/longest streaks from canonical day records.
Translating a growing catalog without blocking editors
Editors publish in English; a queue translates in the background with bounded concurrency so OpenAI rate limits never block the CMS, and failed jobs self-heal on the next deploy.
My Contributions
- Everything: product design, Flutter app, API, admin CMS, AWS deployment, store release pipeline
- Firebase→JWT auth bridge with verified-email account linking
- Camera OCR highlight capture with ML Kit and an in-app PDF reader
- Async OpenAI translation queue and S3→SQS→Thumbor media pipeline
Impact
- Live in production at penspace.in, app at v1.3.3 across 23 builds
- Six-language content pipeline operated by a single engineer
- Deep links, push reminders and streaks form a complete retention loop
Lessons Learned
Owning every layer forces honest architecture: each abstraction exists because operating the system alone demanded it, including async everything, self-healing queues, and conventions documented well enough that future contributors inherit a codebase, not tribal knowledge.