Building a mobile app from zero.

I am building a dream journal app from scratch and keeping every step here until it ships to the stores: the decisions, the work done, the mistakes. Each section opens when you click it.

I am building a dream journal app from scratch. The whole process is here.

Overall progresscalculating
0 / 0 steps done

What does the app do?

Write it, interpret it, collect it, match with people who dreamt something similar.

Dream journal

In the morning you write your dream or tell it out loud. It is saved with mood, clarity and themes, and over time the patterns show up.

AI interpretation

Three source-based lenses: classical interpretation, a Sufi reading, a psychological reading. Structured, with a named source and a stated limit.

Similar-dream matching

Anonymous matching with people who had the same kind of dream, meeting only with mutual consent, a chat that starts under a nickname.

The steps

Click to open: the why and what was done.

Why

An app for people who want to keep their dreams instead of losing them, make sense of them, and talk to someone who dreamt the same thing. There are three cores: the journal, the interpretation and the matching. In the morning you write or tell your dream; the AI interprets it through three source-based lenses; over time you see your own patterns; if you want, you move into a chat that starts anonymously with someone who had a similar dream. We are not drawing the free and premium limits yet; we will learn them from real usage and draw them later.

What the app will include

  1. Dream journal: record by text or voice (speech to text), date, mood, tags; search and archive
  2. AI interpretation: three lenses (classical interpretation · Sufi reading · psychological reading); symbols, emotions, themes, suggestions and a source note
  3. Symbol cards: a short, sourced explanation for every symbol in your dream
  4. Patterns: recurring symbols and emotions over time, weekly and monthly summaries, charts
  5. Visualisation: an AI-generated image of the dream (optional), a shareable dream card
  6. Reminder: a morning "write your dream" notification, one-tap capture on waking
  7. Privacy: your dreams are yours; delete the account and all data from inside the app
  8. Matching and chat: meeting people with similar dreams (below)
  9. Later releases: lucid dreaming tools, sleep data, English

How people will be matched

  1. Every dream becomes a "dream card" with no identity attached: symbols, themes, mood
  2. The system finds similar cards; identical words are not required, it looks at meaning
  3. You get an anonymous preview: "there is a dream similar to yours" and a summary of the card; no personal details
  4. If you are interested you send a request; the chat opens only if the other side also accepts (mutual consent)
  5. The chat starts with a nickname; identities are shared only if both sides want to, whenever they want
  6. 18+; block and report are one tap away; sensitive-content filter; delete the chat whenever you like
  7. Matching is entirely optional: turn it off and your dreams are never shown to anyone
Dream journalAI interpretationSymbol cardsPatternsMatchingChat

Why

A web-based core (Next.js) wrapped in a mobile shell (WebView): a change does not wait for store review every time, while store-bound work such as subscriptions and notifications stays in the shell. We started small and cheap: a single Lightsail server in Frankfurt (Node 22 + PostgreSQL 16 + pgvector + nginx) behind Cloudflare; media on S3 + CloudFront with signed links; e-mail through SES. When it grows, the database and the cache move out and the architecture stays the same.

The chosen stack

  1. Server: AWS Lightsail (Frankfurt, 2 GB), Ubuntu 24.04 — one box, static IP
  2. App: Node.js 22 + pm2; Next.js core; mobile shell as WebView (iOS/Android)
  3. Database: PostgreSQL 16 + pgvector (similar-dream search) — on the same server for now
  4. Edge: Cloudflare (DNS, proxy, WAF, rate limiting); the origin answers Cloudflare only
  5. Media: private S3 bucket + CloudFront with time-limited signed links
  6. E-mail: Amazon SES; notifications: push through the shell
  7. AI: a provider-independent layer (the app does not change when the model does)
  8. Subscriptions: Apple / Google in-app purchase, RevenueCat
  9. Queue: PostgreSQL-based to start (pg-boss, for AI jobs longer than 95 s), SSE or polling to the client; Redis + BullMQ when it grows
LightsailCloudflareNode 22Next.jsPostgreSQL 16pgvectorS3 + CloudFrontSESRevenueCatWebView

Why

A solid floor before any application code: one small server, managed DNS and CDN, automatic backups, and the habit of putting every step through an independent review. Because it will carry personal data such as dreams, voice and photos, security and least privilege were set up from the very beginning. The design and the installation went through three rounds of adversarial review with AI; every hole found was closed. When the setup was done, one more review asked "does this hold at a million users?"; the seven items it produced (33–39) were implemented.

What was done · in order

  1. Register the domain, confirm the e-mail
  2. Add the domain to Cloudflare, change the nameservers
  3. Install the AWS CLI, create a fresh SSH key
  4. Create the Lightsail server, attach a static IP
  5. Set up the firewall (SSH from one IP, 80/443)
  6. Enable automatic snapshots
  7. Write and run the server bootstrap script
  8. Configure swap and automatic patching
  9. Install fail2ban and ufw
  10. Install Node.js and pm2
  11. Install PostgreSQL 16 + pgvector (localhost only)
  12. Set up secrets and .env: generate the DB password on the server, .env 640 root:app, keep keys in the keychain, pipe them over stdin
  13. Install nginx, add a placeholder page and /health
  14. Clean up Cloudflare DNS, enter the A records (orange cloud)
  15. Install SSL (Let's Encrypt), switch Cloudflare to Full strict
  16. Set up HTTP → HTTPS redirection
  17. Create the S3 bucket and lock it down (no public access)
  18. Create IAM identities (app + backup, least privilege)
  19. Set up CloudFront, generate the signed-URL key pair
  20. Set up SES e-mail, add DKIM/SPF/DMARC
  21. Write the nightly backup script and its cron job
  22. Narrow the origin down to Cloudflare IPs only
  23. Add a catch-all (no answer for unknown domains)
  24. Turn on Authenticated Origin Pulls
  25. Disable IPv6, disable root login
  26. Set up alerts and a daily health check
  27. Set up rate limiting (nginx + Cloudflare)
  28. Set up security headers and CSP
  29. Harden Cloudflare (Always Use HTTPS, HSTS, min TLS 1.2, Bot Fight Mode)
  30. Run the evidence script
  31. Adversarial review (3 rounds) and fixes
  32. Controlled restart, final test
  33. Make rate limiting user-based (token/session; IP only for anonymous), strict limit for /api/auth/
  34. Disable proxy buffering for streaming (SSE); decide queue + streaming for long AI jobs
  35. Test Cloudflare with mobile app user agents (the WebView must not see a challenge)
  36. Tune PostgreSQL for 2 GB, pg_stat_statements; Node heap; pm2 ecosystem reads .env itself (no env_file); log rotation
  37. Install pgBackRest: encrypted S3 backup + WAL archive (loss window ≤ 5 min), daily cron
  38. A real monthly restore drill; add backup health to the daily check
  39. Recovery runbook and a bus-factor package (offline secrets, MFA, domain lock)
Preparation · accounts, tools, domainfor starting from zero · P1–P10
  1. Opening an AWS accountSign-up, card verification, Basic support plan; MFA on root, an IAM user for daily work, a budget alarm, region Frankfurt.
  2. Installing and connecting the AWS CLIInstall on macOS/Windows/Linux; create an access key, run aws configure, verify with get-caller-identity; use profiles for multiple accounts.
  3. Creating an SSH keyssh-keygen -t ed25519 (works in PowerShell on Windows too); the private key stays with you, the public key goes to the server; one command with ~/.ssh/config.
  4. Creating the Lightsail instance from the consoleCreate instance → Frankfurt → Ubuntu 24.04 → upload your key → 2 GB plan; attach a static IP, restrict SSH to your own IP, enable automatic snapshots.
  5. Registering the domainPick a registrar, turn on WHOIS privacy and auto-renew; click the ICANN verification in the registration e-mail immediately (otherwise the domain is suspended); keep the transfer lock on.
  6. Cloudflare account and adding the domainSign up + 2FA; Add a domain → Free; delete the junk records the scan brings in; note the two nameservers you are given.
  7. Pointing the nameserversIn the registrar panel: Nameservers → custom → enter Cloudflare's two; if the panel has no such field, open a support ticket (sample text in the MD); check propagation with dig.
  8. Connecting the domain to the serverA records for @, www, api → server IP (orange cloud); SPF/DMARC protection; SSL mode Full, then Full (strict) once the certificate is issued.
  9. Cloudflare API tokenCustom token: DNS, Zone Settings, Zone WAF, SSL and Certificates, Zone Read; scoped to that one domain; keep it in the keychain.
  10. First connection and the setup scriptConnect over ssh, upload the script, run it with nohup, watch the log; when .bootstrap-done appears, /health answers.
The full steps, the commands and the sample support ticket are in this step’s MD file.
LightsailCloudflarePostgreSQL 16pgvectorCloudFrontSESLet's EncryptAdversarial review
Monthly cost is at the smallest-plan level; the media CDN and e-mail start on the free tier. The video for this step is out.
The notes and everything done in this step, in a single file (Markdown).

Why

This site will publish dream symbols and their meanings, the app and the site will support several languages, and people now ask their questions to chatbots as well. The search foundation had to be laid before the content arrived: a site that returns 200 for every URL, a www duplicate and content buried in JavaScript leave a debt that is hard to clean up later. Every tag, the sitemap, robots, image metadata and structured data are generated from a single language-aware configuration file; adding a page, a language or an image is one line. Google's SEO starter guide and its AI guidance were compared item by item and the gaps were closed.

What was done · in order

  1. Real 404 for unknown URLs (SPA fallback removed), a plain 404 page, noindex
  2. www → apex 301, canonical on every page, charset header
  3. One configuration file: pages, languages, translations, images → every tag is generated from it
  4. hreflang (tr + x-default), og:locale, language prefixes /tr/…, /en/…; no automatic redirect by language
  5. sitemap.xml (with hreflang and image extensions) and robots.txt
  6. OG and Twitter cards, a 1200×630 image generated from a template for every page
  7. JSON-LD: Organization, Person, WebSite, WebPage/Article/AboutPage, BreadcrumbList, ImageObject, VideoObject
  8. Build-log steps pre-rendered into raw HTML; step titles are real h3 elements
  9. Content with h2 headings on the home page; an About page (who, how, why)
  10. Image pipeline: alt and caption per language from JSON, WebP + dimensions, descriptive file names, image sitemap, LCP preload
  11. 30-day cache for static assets; HTML revalidated on every visit
  12. llms.txt + llms-full.txt, RSS, site.webmanifest + icons
  13. IndexNow (Bing, Yandex, Naver instantly); Search Console connected
  14. AI bots: Content-Signal and explicit permission (search + training), Cloudflare managed robots turned off
  15. Compared against Google's SEO starter guide and AI guidance, gaps closed
  16. A post-build SEO regression test: canonical, hreflang reciprocity, single h1, JSON-LD targets, OG and image files, alt text, sitemap ↔ pages; publishing stops on any error
  17. The URL and content model for the dream dictionary: /{lang}/{section}/{slug} — the section name changes with the language (ruya, dreams, traumdeutung, sonnik…), 12 languages in stages (TR/EN/DE first), ASCII slugs, a page pattern (short answer + three lenses), a source and citation model, topic clusters (internal linking), Search Console → content loop
SEOGEOhreflangJSON-LDsitemapIndexNowi18n
The notes and everything done in this step, in a single file (Markdown).

Why

We did not want to ship a new mobile release just to add an interpretation school or change a prompt: the schools, the prompts and the model list live in the database, change in the panel, and the app sees them on its next request. Because a prompt changes the behaviour of the product, it is versioned and tried out before it goes live; the log records which version and model produced every answer. Everything the user sees (name, description, cover, slug, prompt) is per language, while the system identifier stays shared. Since the panel is a separate attack surface, identity, sessions, CSRF and input validation were built strictly from the start, and an AI agent that had never seen the code was asked to break it before it went live.

What was done · in order

  1. Next.js 16 (standalone) + PostgreSQL + Drizzle; served at /appsomnio, noindex, every page redirects to login when signed out
  2. argon2id passwords, constant-time login; session token hashed in the DB, httpOnly + Secure + SameSite=strict, 12-hour sliding; sign out of all sessions
  3. Two layers of login limiting (nginx + app), Origin check for CSRF, roles, audit log, admin-specific CSP/HSTS headers
  4. Interpretation categories in a single form: shared settings + language tabs (name, description, note, cover, SEO slug)
  5. System key derived from the title automatically; per-language slug (Turkish/German/Cyrillic/Arabic/CJK → Latin), editable, unique within a language
  6. Per-language cover image: 1600 px WebP in the browser, type checked by byte signature on the server, content-hashed file name, CDN cache; falls back to TR when a language has no cover
  7. Prompt template = category × language, * as default; draft → active → archived, a single active version enforced by a DB constraint; placeholder chips, JSON output schema
  8. Try it: a real Gemini call, the rendered prompts, duration, tokens, cost (thinking tokens included), daily quota
  9. AI models in the panel (identity, price, default); every call recorded in the ai_runs log
  10. Public /api/categories: the name, cover, slug and active-template info in the requested language (the proof that it is dynamic)
  11. Design: one CSS token file, an inline SVG icon set, self-hosted fonts, a separate client component per page
  12. Local pipeline: build → standalone → reset the test DB → 80 end-to-end API checks → headless screenshots
  13. An independent "break this" review: 12 findings (including a critical SVG XSS) closed; an allowlist sanitiser instead of a blocklist
  14. Deploy: build on the Mac → release folder → pre-migration backup → migration → pm2 → health → automatic rollback; Linux native prebuilds inside the package
  15. MD: a guide with the full source code that you can hand to an AI and adapt to your own project
Next.jsPostgreSQLDrizzleargon2Geminiprompt versioningi18nsecuritydeploy
The notes and everything done in this step, in a single file (Markdown).