Prd v2
Current System Overview
ZapiszPrzepis is a personal recipe-collection PWA deployed on Cloudflare Workers (OpenNext), accessible at zapiszprzepis.pl. Stack: Next.js, Supabase (PostgreSQL + magic-link auth + Storage), Inngest (async job runner), Firecrawl/OpenAI (recipe extraction), pnpm.
User base: a single private user (the author’s mother) on Android Chrome; per-user data isolation via Supabase RLS.
Core functionality today:
- RecipeSearch — filters the user’s own recipe collection by title or ingredient (
?q=URL param; client-side.includes()over fetched rows) addRecipeFromUrl(url)— server action that deduplicates by URL, inserts intorecipe_shares, and triggers the Inngest extraction event- Inngest extraction pipeline — fetches page content (Firecrawl / yt-dlp) → extracts recipe (OpenAI) → normalizes ingredients → saves to the
recipestable - Auth — fully wired magic-link; long-lived session; SSR middleware
- Web Share Target — registered PWA target; the user shares a URL from any app and
addRecipeFromUrlis invoked
Problem Statement & Motivation
The app has no path for discovering new recipes. The user can only browse her own saved collection — to find a recipe she must leave the app, search externally, and share the URL back via Web Share Target.
The specific gap: the user’s daily workflow is to look up a dish by name elsewhere (Facebook, search engine), then share the link back. The “elsewhere” step is friction — she sometimes forgets, sometimes loses the link.
Trigger: A dedicated recipe-search API, accessible from the existing serverless runtime via standard HTTP fetch, is now available. The existing addRecipeFromUrl pipeline already handles saving from any URL — the only missing piece is an in-app discovery UI and the API call.
User & Persona
Unchanged — the author’s mother (Android, Chrome). She gains an optional discovery path: instead of sharing from Facebook or YouTube, she can search directly in the app by typing or speaking the name of a dish. All existing paths (Web Share Target, direct URL sharing) remain available.
Success Criteria
Primary
- The user types a query in the app and within 2 seconds sees a list of internet recipe results; clicking “Zapisz” at a result triggers the existing pipeline and the recipe appears in the collection exactly as it does when sharing from a browser.
Secondary
- Voice search works on Chrome for Android — the user speaks a dish name, the text field fills, and results appear after speech recognition completes.
Guardrails
- Existing RecipeSearch (filtering the user’s own collection) works without any change.
addRecipeFromUrland the Inngest pipeline behave identically to the Web Share Target flow — no regression.- The external search API credentials never reach the client; all search calls are made server-side.
- The monthly search request quota (1,000 requests on the free tier) is not exceeded — search triggers only on explicit submit, never on keystroke input.
User Stories
US-01: User discovers recipes from the home screen
- Given a logged-in user on the home screen
- When they type a dish name into the discovery search field and press Enter (or tap the submit button)
- Then they see a list of internet recipe results — each card showing a title, thumbnail, source domain, and snippet — within 2 seconds
Acceptance Criteria
- Results appear within 2 seconds of submit
- Cards whose URLs are already in the user’s collection display a “Już zapisano” indicator
- Search does not trigger while typing — only on explicit submit (Enter or button tap)
- Before: user had to leave the app and use Web Share Target; now the search happens inside the app
US-02: User searches by voice
- Given a logged-in user on the home screen using a browser that supports voice input
- When they tap the microphone icon and speak a dish name in Polish
- Then the spoken text fills the search field and the search executes automatically after recognition completes
Acceptance Criteria
- The microphone button is present and functional on Chrome for Android
- Spoken Polish is correctly transcribed into the search field
- If voice input is not supported (non-supporting browser or non-secure context), the microphone button is hidden — no error is shown
US-03: User previews a discovery result before saving
- Given a user viewing a list of discovery results
- When they tap a result card
- Then they see a preview page showing the title, thumbnail, source domain, and snippet from the search result, with a “Zapisz” button and a back arrow; the full recipe extraction pipeline does NOT run at this point
Acceptance Criteria
- Preview page is visually distinct from saved-recipe pages (light beige background)
- No extraction pipeline is triggered by viewing the preview
- Back arrow returns the user to the results list
US-04: User saves a recipe from the preview
- Given a user viewing a recipe preview page
- When they tap “Zapisz”
- Then the button changes to a disabled “Zapisuję… pojawi się w Moich Przepisach za chwilę” state; the existing extraction pipeline starts; the user can continue reading the preview or navigate back
Acceptance Criteria
- Button becomes disabled immediately on tap — no double-submit
- The recipe appears in the user’s collection via the same pipeline as Web Share Target, with no difference in output
Scope of Change
- [new] Discovery search bar on the home screen (text input + submit button)
- [new] Voice input button adjacent to the search field
- [new] Server-side action that calls an external recipe search API and returns a result list with title, thumbnail, source domain, and snippet per result
- [new] Deduplication check that marks results already present in the user’s collection with “Już zapisano”
- [new] Recipe preview page showing external result data (title, thumbnail, source, snippet) with a “Zapisz” button and back navigation
- [modified] Home screen layout — logout moved to icon in top corner; “Moje przepisy” button enlarged and repositioned as the primary element; discovery search added below it
- [preserved] RecipeSearch — existing own-collection filter (
?q=param) unchanged and on a fully independent path - [preserved]
addRecipeFromUrl(url)and the Inngest extraction pipeline — called from the preview page identically to the Web Share Target path; no signature or behavior change
Constraints & Compatibility
- Backward compatibility — no URL structure, API contracts, or data formats change for existing flows. RecipeSearch
?q=param is unchanged.addRecipeFromUrlsignature and behavior are unchanged. Web Share Target registration is unchanged. - Data migration — none required; no schema changes, no backfill.
- Existing integrations — the Inngest pipeline, content-fetching, and recipe-extraction steps must continue working without modification.
- Preserved behavior:
- RecipeSearch (own-collection filtering) runs on an entirely independent path from discovery search; changes to one must not affect the other.
addRecipeFromUrlis called from the new preview page with the same argument and produces the same result as the Web Share Target invocation.- Per-user data isolation (RLS) is unchanged — recipe records are created with the session user’s identity, as before.
- The external search API key is kept exclusively server-side; it must never appear in any client-delivered asset, log, or response.
- Search triggers only on explicit user action (Enter or submit button) — never on keystroke — to stay within the monthly request quota.
Business Logic Changes
No domain logic change. This is an infrastructure and UI change — a new URL source (external search results) feeds the existing extraction pipeline. The domain rule is unchanged:
A recipe URL saved by the user passes through content fetching, recipe extraction, ingredient normalization, and archiving; the result is classified, structured, and stored in the user’s collection.
The new discovery path supplies URLs; all domain decisions (category classification, language normalization, ingredient structuring) are handled by the existing pipeline without modification.
Access Control Changes
No access control changes — current model preserved.
Discovery search is available only to authenticated users (same magic-link session). No new roles. No public access to search results. The external search API key is stored as a server-side secret and never exposed to the client — following the same pattern as the existing content-fetching and extraction API keys.
Non-Goals
- Pagination of search results — the external API returns 10 results per query by default; pagination will be revisited if the limit proves insufficient in practice.
- Category or diet filters in the search UI — the external search API does not expose recipe-category filtering; no filter controls in the UI.
- Caching of search results — unnecessary for a single-user app at this scale.
- Voice search on iOS / Safari — voice input does not have full support on iOS Safari; no guarantee on iOS devices.
- Sorting, rating, or pre-filtering of results before saving — no thumbs-up/down, star rating, or manual re-ranking.
- Full recipe extraction at preview time — the extraction pipeline runs only after the user explicitly taps “Zapisz”, consistent with the Web Share Target flow.
Open Questions
- What happens when voice input is unavailable? (Non-HTTPS context, or a browser that does not support the voice input API) — should the microphone button be hidden silently, or rendered in a disabled state with a tooltip? To be decided at implementation time.