Vibecode Pic-Time
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Pic-Time, host client galleries with favorites, downloads, and simple proofing. The hard boundary is gallery delivery, store automation, print labs, marketing, and support, plus image pipeline quality, models, and workflow polish.
You are building a lean indie version of Pic-Time. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Pic-Time · indie build A client gallery platform where the engineering is delivering large images fast enough that a gallery of eight hundred photographs is browsable, and the discipline is that a photographer's delivery to a client is a professional commitment with a deadline attached. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Originals in object storage, derivatives generated once and cached | regenerating a preview per view is slow and expensive at gallery scale | | Delivery | A content network in front, with signed access per gallery | a gallery is browsed on a phone by people who will not wait | | Processing | A worker queue generating sizes on upload, resumable | eight hundred images is an hour of work and it will be interrupted | | Access | Per-gallery links with optional password and expiry | clients share the link, which is the point and also the risk | | Runtime | TypeScript with PostgreSQL | galleries, selections and orders are relational | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the processing workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Galleries, selections and orders with per-visitor keys. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Object storage with a content network** · storage is cheap; bandwidth is the real cost - Why: Delivering hundreds of images per gallery is a bandwidth problem before it is anything else, and serving directly from storage is slow and expensive. - Get it: A storage provider with a network in front, supporting signed access and direct uploads. - Verify: A signed image address works and stops working after expiry - [ ] **A real shoot of at least six hundred images** · free if you have one - Why: Upload resumption, processing time and gallery scrolling are all uninteresting below a few hundred images and become the whole problem above it. - Get it: A real set of full resolution files, including a mixture of orientations and at least one corrupt file. - Verify: Six hundred full resolution images are available including a deliberately corrupt one - [ ] **A phone on a mobile connection** · free - Why: Gallery performance is judged on a phone by a client who will not wait, and a desktop connection hides every problem that matters. - Get it: Any phone, tested away from a fast local network. - Verify: A gallery is browsed end to end on mobile data ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Print fulfilment and lab integration. - Image editing. - Studio management with contracts and scheduling. - Screenshot prevention. - gallery delivery, store automation, print labs, marketing, and support - proprietary raw-processing quality - cloud sync and sharing - large AI models - camera and print ecosystem If one of those is essential to you, that is the reason to keep paying for Pic-Time, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Pic-Time The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me client photo galleries with proofing, downloads and delivery that does not fall over to replace Pic-Time. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Originals in object storage, derivatives generated once and cached · regenerating a preview per view is slow and expensive at gallery scale - Delivery: A content network in front, with signed access per gallery · a gallery is browsed on a phone by people who will not wait - Processing: A worker queue generating sizes on upload, resumable · eight hundred images is an hour of work and it will be interrupted - Access: Per-gallery links with optional password and expiry · clients share the link, which is the point and also the risk - Runtime: TypeScript with PostgreSQL · galleries, selections and orders are relational ### Data model (create this before Phase 1) - galleries(id, client_ref, title, shoot_date, access_kind, password_hash, expires_at, download_policy, watermark_policy) - photos(id, gallery_id, original_key, filename, captured_at, width, height, orientation, hidden) - derivatives(photo_id, size, key, bytes, generated_at) · generated once, cached, regenerable - selections(gallery_id, visitor_id, photo_id, kind, at) · kind is favourite, hidden or comment_anchor - comments(id, photo_id, visitor_id, body, at) - downloads(id, gallery_id, photo_id, visitor_id, size, at) · so a photographer knows what was taken - orders(id, gallery_id, items_json, total_minor, status, placed_at) · print orders where offered - A visitor identity is per gallery rather than global, because clients share links with family and each person's favourites should be their own. ### Phase 1 · Uploading a shoot without losing it Build: Direct-to-storage uploads with resumption, and a worker queue generating derivative sizes. Handle an interrupted upload session and a browser closed midway. Report progress honestly including failures. Trap: Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. Done when: Eight hundred images upload with the browser closed midway and resume without duplicates. Derivative generation runs in workers and survives a restart. Failed images are reported individually rather than failing the batch. Progress reflects actual completion rather than upload alone. Do not build yet: No gallery, access or selection. ### Phase 2 · A gallery that is fast on a phone Build: Serve appropriately sized derivatives per viewport with lazy loading. Preserve colour profile and orientation. Measure time to first images and scrolling smoothness on a real phone on a real connection. Trap: Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. Done when: A gallery of eight hundred images scrolls smoothly on a phone on a mobile connection, measured rather than assumed. Images are served at a size appropriate to the viewport. Colour profile and orientation are preserved so images look as intended. Time to first meaningful images is measured and stated. Do not build yet: No access control, selections or downloads yet. ### Phase 3 · Who can see it, and for how long Build: Per-gallery links with an unguessable token, optional password and an expiry. Signed access to derivatives so an image address is not permanently public. Handle a client sharing the link, which is expected. Trap: Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. Done when: Image addresses are signed and stop working when gallery access expires, tested by copying one. A password-protected gallery cannot be browsed without it including its images. Expiry is set by default rather than optional. Sharing the link works as intended, since that is what clients do. Do not build yet: No selections, downloads or orders yet. ### Phase 4 · Favourites, per person Build: Per-visitor selections within a gallery, with a durable identity requiring no account. Support comments anchored to photographs. Give the photographer a combined view showing who selected what. Trap: Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. Done when: Two visitors' selections coexist without overwriting, tested with two browsers. Selections persist across sessions without an account. The photographer sees selections attributed per visitor and combined. A visitor can name themselves so the photographer knows whose list is whose. Do not build yet: No downloads or orders yet. ### Phase 5 · Downloads, watermarks and getting paid Build: Per-gallery download policy by size, with optional watermarking on previews and never on purchased downloads. Package a bulk download without holding it in memory. Record what was downloaded. Trap: Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed. Done when: A bulk download of a large gallery streams without exhausting memory, tested at full size. Watermarks apply to previews per policy and never to paid downloads. Download policy is per gallery and enforced server side. Downloads are recorded so the photographer knows what the client took. Do not build yet: Nothing after this. ### Out of scope (and why) - Print fulfilment and lab integration, which is where the hosted products make their money and involves real logistics. - Image editing, which happens before delivery in a different tool entirely. - Studio management with contracts and scheduling, which is a separate product for the same customer. - Any serious attempt to prevent screenshots, which is not achievable and degrades the experience for paying clients. ### README must contain - That uploads go directly to storage and processing runs in resumable workers, because a big shoot will be interrupted - That image addresses are signed and expire with gallery access, not just the gallery page - That selections are keyed per visitor so two family members do not overwrite each other - That bulk downloads are streamed rather than built in memory - That watermarks apply to previews and never to paid downloads ===== AGENTS.md ===== # Agent instructions · Pic-Time indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Originals in object storage, derivatives generated once and cached, A content network in front, with signed access per gallery, A worker queue generating sizes on upload, resumable, Per-gallery links with optional password and expiry, TypeScript with PostgreSQL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. ===== BUILD_PLAN.md ===== # Build plan · Pic-Time A client gallery platform where the engineering is delivering large images fast enough that a gallery of eight hundred photographs is browsable, and the discipline is that a photographer's delivery to a client is a professional commitment with a deadline attached. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Uploading a shoot without losing it Eight hundred images upload and process without the photographer babysitting it. ### Steps 1. Upload direct to storage, resumable Through the application server is how a big shoot fails. Files: `src/upload.ts` 2. Generate derivatives in workers Synchronous processing ties up the request and loses on restart. Files: `src/worker.ts` 3. Report per-image failures One corrupt file must not fail eight hundred. Files: `src/processing.ts` ### Done when - [ ] Eight hundred images upload with interruption - [ ] Derivative generation survives a restart - [ ] Failures are reported per image - [ ] Progress reflects processing, not just upload ### Watch out - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. ## Phase 2 · A gallery that is fast on a phone A client scrolls hundreds of images without waiting. ### Steps 1. Serve per-viewport sizes Full resolution scaled in the browser is why galleries feel broken. Files: `src/serve.ts` 2. Preserve profile and orientation A photographer's colours are the product. Files: `src/derivatives.ts` 3. Measure on a real device A desktop connection hides everything that matters here. Files: `tests/performance.md` ### Done when - [ ] Eight hundred images scroll smoothly on a phone - [ ] Sizes match the viewport - [ ] Colour profile and orientation are preserved - [ ] Time to first images is measured ### Watch out - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. ## Phase 3 · Who can see it, and for how long Gallery access is controlled without making it awkward for the client. ### Steps 1. Sign image addresses too Protecting only the page leaves the photographs public. Files: `src/access.ts` 2. Set an expiry by default Galleries accumulate and storage costs money indefinitely. Files: `src/galleries.ts` 3. Expect the link to be shared It is how families see the photographs. Design for it. Files: `src/access.ts` ### Done when - [ ] Image addresses expire with gallery access - [ ] A password gates images as well as the page - [ ] Expiry is set by default - [ ] Link sharing works as intended ### Watch out - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. ## Phase 4 · Favourites, per person Two family members can each pick their favourites without overwriting each other. ### Steps 1. Key selections to a per-gallery visitor Not to the gallery. The second person overwrites otherwise. Files: `src/selections.ts` 2. Persist without an account Clients will not sign up to pick photographs. Files: `src/visitors.ts` 3. Attribute and combine for the photographer Whose favourites these are is the useful part. Files: `src/review.ts` ### Done when - [ ] Two visitors' selections coexist - [ ] Selections persist without an account - [ ] The photographer sees attribution and a combined view - [ ] A visitor can name themselves ### Watch out - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. ## Not in this build - Print fulfilment and lab integration. - Image editing. - Studio management with contracts and scheduling. - Screenshot prevention. ## After v1, if you want it - Client proofing rounds with a stated deadline, which is the workflow photographers actually run - Storage lifecycle moving old galleries to cheaper tiers, since bandwidth and storage are the ongoing cost - A photographer-facing view of which galleries are approaching expiry ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Galleries, client references and selections. DATABASE_URL=postgres://localhost:5432/galleries # Required. Originals. Derivatives may live separately since they are regenerable. STORAGE_BUCKET=s3://gallery-originals # Required. Where signed derivative addresses are served from. CDN_BASE_URL=https://img.example.com # Required. Long enough to browse a gallery, short enough that a copied address expires. SIGNED_URL_TTL_MINUTES=60 # Required. Galleries accumulate and storage costs money indefinitely. GALLERY_DEFAULT_EXPIRY_DAYS=365 # Required. Present as a constant. A client who paid gets the file. WATERMARK_PAID_DOWNLOADS=never
You are building a lean indie version of Pic-Time. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Pic-Time · indie build A client gallery platform where the engineering is delivering large images fast enough that a gallery of eight hundred photographs is browsable, and the discipline is that a photographer's delivery to a client is a professional commitment with a deadline attached. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Originals in object storage, derivatives generated once and cached | regenerating a preview per view is slow and expensive at gallery scale | | Delivery | A content network in front, with signed access per gallery | a gallery is browsed on a phone by people who will not wait | | Processing | A worker queue generating sizes on upload, resumable | eight hundred images is an hour of work and it will be interrupted | | Access | Per-gallery links with optional password and expiry | clients share the link, which is the point and also the risk | | Runtime | TypeScript with PostgreSQL | galleries, selections and orders are relational | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the processing workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Galleries, selections and orders with per-visitor keys. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Object storage with a content network** · storage is cheap; bandwidth is the real cost - Why: Delivering hundreds of images per gallery is a bandwidth problem before it is anything else, and serving directly from storage is slow and expensive. - Get it: A storage provider with a network in front, supporting signed access and direct uploads. - Verify: A signed image address works and stops working after expiry - [ ] **A real shoot of at least six hundred images** · free if you have one - Why: Upload resumption, processing time and gallery scrolling are all uninteresting below a few hundred images and become the whole problem above it. - Get it: A real set of full resolution files, including a mixture of orientations and at least one corrupt file. - Verify: Six hundred full resolution images are available including a deliberately corrupt one - [ ] **A phone on a mobile connection** · free - Why: Gallery performance is judged on a phone by a client who will not wait, and a desktop connection hides every problem that matters. - Get it: Any phone, tested away from a fast local network. - Verify: A gallery is browsed end to end on mobile data ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Print fulfilment and lab integration. - Image editing. - Studio management with contracts and scheduling. - Screenshot prevention. - gallery delivery, store automation, print labs, marketing, and support - proprietary raw-processing quality - cloud sync and sharing - large AI models - camera and print ecosystem If one of those is essential to you, that is the reason to keep paying for Pic-Time, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Pic-Time The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me client photo galleries with proofing, downloads and delivery that does not fall over to replace Pic-Time. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Originals in object storage, derivatives generated once and cached · regenerating a preview per view is slow and expensive at gallery scale - Delivery: A content network in front, with signed access per gallery · a gallery is browsed on a phone by people who will not wait - Processing: A worker queue generating sizes on upload, resumable · eight hundred images is an hour of work and it will be interrupted - Access: Per-gallery links with optional password and expiry · clients share the link, which is the point and also the risk - Runtime: TypeScript with PostgreSQL · galleries, selections and orders are relational ### Data model (create this before Phase 1) - galleries(id, client_ref, title, shoot_date, access_kind, password_hash, expires_at, download_policy, watermark_policy) - photos(id, gallery_id, original_key, filename, captured_at, width, height, orientation, hidden) - derivatives(photo_id, size, key, bytes, generated_at) · generated once, cached, regenerable - selections(gallery_id, visitor_id, photo_id, kind, at) · kind is favourite, hidden or comment_anchor - comments(id, photo_id, visitor_id, body, at) - downloads(id, gallery_id, photo_id, visitor_id, size, at) · so a photographer knows what was taken - orders(id, gallery_id, items_json, total_minor, status, placed_at) · print orders where offered - A visitor identity is per gallery rather than global, because clients share links with family and each person's favourites should be their own. ### Phase 1 · Uploading a shoot without losing it Build: Direct-to-storage uploads with resumption, and a worker queue generating derivative sizes. Handle an interrupted upload session and a browser closed midway. Report progress honestly including failures. Trap: Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. Done when: Eight hundred images upload with the browser closed midway and resume without duplicates. Derivative generation runs in workers and survives a restart. Failed images are reported individually rather than failing the batch. Progress reflects actual completion rather than upload alone. Do not build yet: No gallery, access or selection. ### Phase 2 · A gallery that is fast on a phone Build: Serve appropriately sized derivatives per viewport with lazy loading. Preserve colour profile and orientation. Measure time to first images and scrolling smoothness on a real phone on a real connection. Trap: Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. Done when: A gallery of eight hundred images scrolls smoothly on a phone on a mobile connection, measured rather than assumed. Images are served at a size appropriate to the viewport. Colour profile and orientation are preserved so images look as intended. Time to first meaningful images is measured and stated. Do not build yet: No access control, selections or downloads yet. ### Phase 3 · Who can see it, and for how long Build: Per-gallery links with an unguessable token, optional password and an expiry. Signed access to derivatives so an image address is not permanently public. Handle a client sharing the link, which is expected. Trap: Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. Done when: Image addresses are signed and stop working when gallery access expires, tested by copying one. A password-protected gallery cannot be browsed without it including its images. Expiry is set by default rather than optional. Sharing the link works as intended, since that is what clients do. Do not build yet: No selections, downloads or orders yet. ### Phase 4 · Favourites, per person Build: Per-visitor selections within a gallery, with a durable identity requiring no account. Support comments anchored to photographs. Give the photographer a combined view showing who selected what. Trap: Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. Done when: Two visitors' selections coexist without overwriting, tested with two browsers. Selections persist across sessions without an account. The photographer sees selections attributed per visitor and combined. A visitor can name themselves so the photographer knows whose list is whose. Do not build yet: No downloads or orders yet. ### Phase 5 · Downloads, watermarks and getting paid Build: Per-gallery download policy by size, with optional watermarking on previews and never on purchased downloads. Package a bulk download without holding it in memory. Record what was downloaded. Trap: Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed. Done when: A bulk download of a large gallery streams without exhausting memory, tested at full size. Watermarks apply to previews per policy and never to paid downloads. Download policy is per gallery and enforced server side. Downloads are recorded so the photographer knows what the client took. Do not build yet: Nothing after this. ### Out of scope (and why) - Print fulfilment and lab integration, which is where the hosted products make their money and involves real logistics. - Image editing, which happens before delivery in a different tool entirely. - Studio management with contracts and scheduling, which is a separate product for the same customer. - Any serious attempt to prevent screenshots, which is not achievable and degrades the experience for paying clients. ### README must contain - That uploads go directly to storage and processing runs in resumable workers, because a big shoot will be interrupted - That image addresses are signed and expire with gallery access, not just the gallery page - That selections are keyed per visitor so two family members do not overwrite each other - That bulk downloads are streamed rather than built in memory - That watermarks apply to previews and never to paid downloads ===== AGENTS.md ===== # Agent instructions · Pic-Time indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Originals in object storage, derivatives generated once and cached, A content network in front, with signed access per gallery, A worker queue generating sizes on upload, resumable, Per-gallery links with optional password and expiry, TypeScript with PostgreSQL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. ===== BUILD_PLAN.md ===== # Build plan · Pic-Time A client gallery platform where the engineering is delivering large images fast enough that a gallery of eight hundred photographs is browsable, and the discipline is that a photographer's delivery to a client is a professional commitment with a deadline attached. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Uploading a shoot without losing it Eight hundred images upload and process without the photographer babysitting it. ### Steps 1. Upload direct to storage, resumable Through the application server is how a big shoot fails. Files: `src/upload.ts` 2. Generate derivatives in workers Synchronous processing ties up the request and loses on restart. Files: `src/worker.ts` 3. Report per-image failures One corrupt file must not fail eight hundred. Files: `src/processing.ts` ### Done when - [ ] Eight hundred images upload with interruption - [ ] Derivative generation survives a restart - [ ] Failures are reported per image - [ ] Progress reflects processing, not just upload ### Watch out - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. ## Phase 2 · A gallery that is fast on a phone A client scrolls hundreds of images without waiting. ### Steps 1. Serve per-viewport sizes Full resolution scaled in the browser is why galleries feel broken. Files: `src/serve.ts` 2. Preserve profile and orientation A photographer's colours are the product. Files: `src/derivatives.ts` 3. Measure on a real device A desktop connection hides everything that matters here. Files: `tests/performance.md` ### Done when - [ ] Eight hundred images scroll smoothly on a phone - [ ] Sizes match the viewport - [ ] Colour profile and orientation are preserved - [ ] Time to first images is measured ### Watch out - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. ## Phase 3 · Who can see it, and for how long Gallery access is controlled without making it awkward for the client. ### Steps 1. Sign image addresses too Protecting only the page leaves the photographs public. Files: `src/access.ts` 2. Set an expiry by default Galleries accumulate and storage costs money indefinitely. Files: `src/galleries.ts` 3. Expect the link to be shared It is how families see the photographs. Design for it. Files: `src/access.ts` ### Done when - [ ] Image addresses expire with gallery access - [ ] A password gates images as well as the page - [ ] Expiry is set by default - [ ] Link sharing works as intended ### Watch out - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. ## Phase 4 · Favourites, per person Two family members can each pick their favourites without overwriting each other. ### Steps 1. Key selections to a per-gallery visitor Not to the gallery. The second person overwrites otherwise. Files: `src/selections.ts` 2. Persist without an account Clients will not sign up to pick photographs. Files: `src/visitors.ts` 3. Attribute and combine for the photographer Whose favourites these are is the useful part. Files: `src/review.ts` ### Done when - [ ] Two visitors' selections coexist - [ ] Selections persist without an account - [ ] The photographer sees attribution and a combined view - [ ] A visitor can name themselves ### Watch out - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. ## Not in this build - Print fulfilment and lab integration. - Image editing. - Studio management with contracts and scheduling. - Screenshot prevention. ## After v1, if you want it - Client proofing rounds with a stated deadline, which is the workflow photographers actually run - Storage lifecycle moving old galleries to cheaper tiers, since bandwidth and storage are the ongoing cost - A photographer-facing view of which galleries are approaching expiry ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Galleries, client references and selections. DATABASE_URL=postgres://localhost:5432/galleries # Required. Originals. Derivatives may live separately since they are regenerable. STORAGE_BUCKET=s3://gallery-originals # Required. Where signed derivative addresses are served from. CDN_BASE_URL=https://img.example.com # Required. Long enough to browse a gallery, short enough that a copied address expires. SIGNED_URL_TTL_MINUTES=60 # Required. Galleries accumulate and storage costs money indefinitely. GALLERY_DEFAULT_EXPIRY_DAYS=365 # Required. Present as a constant. A client who paid gets the file. WATERMARK_PAID_DOWNLOADS=never
You are building a production product version of Pic-Time. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Pic-Time · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Pic-Time, host client galleries with favorites, downloads, and simple proofing. The hard boundary is gallery delivery, store automation, print labs, marketing, and support, plus image pipeline quality, models, and workflow polish. ## Product outcome Client galleries that stay fast on a phone with hundreds of images, where two family members can each pick their own favourites and a copied image address stops working when the gallery does. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop computer - local photo library - sufficient disk space - optional GPU for AI features ## Explicit non-goals for v1 - Print fulfilment and lab integration. - Image editing. - Studio management with contracts and scheduling. - Screenshot prevention. - gallery delivery, store automation, print labs, marketing, and support - proprietary raw-processing quality - cloud sync and sharing - large AI models - camera and print ecosystem ## Success criteria - Eight hundred images are confirmed to upload with interruption and resume - A gallery is confirmed to scroll smoothly on a phone on mobile data - Image addresses are confirmed to expire with gallery access - Two visitors' selections are confirmed to coexist - A large bulk download is confirmed to stream without exhausting memory ===== BRIEF.md ===== # Build brief · Pic-Time The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me client photo galleries with proofing, downloads and delivery that does not fall over to replace Pic-Time. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Originals in object storage, derivatives generated once and cached · regenerating a preview per view is slow and expensive at gallery scale - Delivery: A content network in front, with signed access per gallery · a gallery is browsed on a phone by people who will not wait - Processing: A worker queue generating sizes on upload, resumable · eight hundred images is an hour of work and it will be interrupted - Access: Per-gallery links with optional password and expiry · clients share the link, which is the point and also the risk - Runtime: TypeScript with PostgreSQL · galleries, selections and orders are relational ### Data model (create this before Phase 1) - galleries(id, client_ref, title, shoot_date, access_kind, password_hash, expires_at, download_policy, watermark_policy) - photos(id, gallery_id, original_key, filename, captured_at, width, height, orientation, hidden) - derivatives(photo_id, size, key, bytes, generated_at) · generated once, cached, regenerable - selections(gallery_id, visitor_id, photo_id, kind, at) · kind is favourite, hidden or comment_anchor - comments(id, photo_id, visitor_id, body, at) - downloads(id, gallery_id, photo_id, visitor_id, size, at) · so a photographer knows what was taken - orders(id, gallery_id, items_json, total_minor, status, placed_at) · print orders where offered - A visitor identity is per gallery rather than global, because clients share links with family and each person's favourites should be their own. ### Phase 1 · Uploading a shoot without losing it Build: Direct-to-storage uploads with resumption, and a worker queue generating derivative sizes. Handle an interrupted upload session and a browser closed midway. Report progress honestly including failures. Trap: Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. Done when: Eight hundred images upload with the browser closed midway and resume without duplicates. Derivative generation runs in workers and survives a restart. Failed images are reported individually rather than failing the batch. Progress reflects actual completion rather than upload alone. Do not build yet: No gallery, access or selection. ### Phase 2 · A gallery that is fast on a phone Build: Serve appropriately sized derivatives per viewport with lazy loading. Preserve colour profile and orientation. Measure time to first images and scrolling smoothness on a real phone on a real connection. Trap: Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. Done when: A gallery of eight hundred images scrolls smoothly on a phone on a mobile connection, measured rather than assumed. Images are served at a size appropriate to the viewport. Colour profile and orientation are preserved so images look as intended. Time to first meaningful images is measured and stated. Do not build yet: No access control, selections or downloads yet. ### Phase 3 · Who can see it, and for how long Build: Per-gallery links with an unguessable token, optional password and an expiry. Signed access to derivatives so an image address is not permanently public. Handle a client sharing the link, which is expected. Trap: Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. Done when: Image addresses are signed and stop working when gallery access expires, tested by copying one. A password-protected gallery cannot be browsed without it including its images. Expiry is set by default rather than optional. Sharing the link works as intended, since that is what clients do. Do not build yet: No selections, downloads or orders yet. ### Phase 4 · Favourites, per person Build: Per-visitor selections within a gallery, with a durable identity requiring no account. Support comments anchored to photographs. Give the photographer a combined view showing who selected what. Trap: Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. Done when: Two visitors' selections coexist without overwriting, tested with two browsers. Selections persist across sessions without an account. The photographer sees selections attributed per visitor and combined. A visitor can name themselves so the photographer knows whose list is whose. Do not build yet: No downloads or orders yet. ### Phase 5 · Downloads, watermarks and getting paid Build: Per-gallery download policy by size, with optional watermarking on previews and never on purchased downloads. Package a bulk download without holding it in memory. Record what was downloaded. Trap: Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed. Done when: A bulk download of a large gallery streams without exhausting memory, tested at full size. Watermarks apply to previews per policy and never to paid downloads. Download policy is per gallery and enforced server side. Downloads are recorded so the photographer knows what the client took. Do not build yet: Nothing after this. ### Out of scope (and why) - Print fulfilment and lab integration, which is where the hosted products make their money and involves real logistics. - Image editing, which happens before delivery in a different tool entirely. - Studio management with contracts and scheduling, which is a separate product for the same customer. - Any serious attempt to prevent screenshots, which is not achievable and degrades the experience for paying clients. ### README must contain - That uploads go directly to storage and processing runs in resumable workers, because a big shoot will be interrupted - That image addresses are signed and expire with gallery access, not just the gallery page - That selections are keyed per visitor so two family members do not overwrite each other - That bulk downloads are streamed rather than built in memory - That watermarks apply to previews and never to paid downloads ===== ARCHITECTURE.md ===== # Architecture · Pic-Time ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Originals in object storage, derivatives generated once and cached | regenerating a preview per view is slow and expensive at gallery scale | | Delivery | A content network in front, with signed access per gallery | a gallery is browsed on a phone by people who will not wait | | Processing | A worker queue generating sizes on upload, resumable | eight hundred images is an hour of work and it will be interrupted | | Access | Per-gallery links with optional password and expiry | clients share the link, which is the point and also the risk | | Runtime | TypeScript with PostgreSQL | galleries, selections and orders are relational | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Upload | Direct, resumable ingestion. | Any storage, provided uploads bypass the application server. | | Derivatives | Sizes generated once and cached. | Any processor, provided generation is asynchronous and resumable. | | Access | Signed gallery and image addresses. | Any scheme, provided image addresses expire with the gallery. | | Selections | Per-visitor favourites without accounts. | Any identity scheme, provided two visitors do not overwrite each other. | | Delivery | Streamed bulk downloads and watermark policy. | Any implementation, provided archives are streamed rather than buffered. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Galleries, client references and selections. - `STORAGE_BUCKET` · required · Originals. Derivatives may live separately since they are regenerable. - `CDN_BASE_URL` · required · Where signed derivative addresses are served from. - `SIGNED_URL_TTL_MINUTES` · required · Long enough to browse a gallery, short enough that a copied address expires. - `GALLERY_DEFAULT_EXPIRY_DAYS` · required · Galleries accumulate and storage costs money indefinitely. - `WATERMARK_PAID_DOWNLOADS` · required · Present as a constant. A client who paid gets the file. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Pic-Time product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Originals in object storage, derivatives generated once and cached, A content network in front, with signed access per gallery, A worker queue generating sizes on upload, resumable, Per-gallery links with optional password and expiry, TypeScript with PostgreSQL. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. - Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed. ===== MILESTONES.md ===== # Delivery milestones · Pic-Time Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Uploading a shoot without losing it Eight hundred images upload and process without the photographer babysitting it. ### Steps 1. Upload direct to storage, resumable Through the application server is how a big shoot fails. Files: `src/upload.ts` 2. Generate derivatives in workers Synchronous processing ties up the request and loses on restart. Files: `src/worker.ts` 3. Report per-image failures One corrupt file must not fail eight hundred. Files: `src/processing.ts` ### Done when - [ ] Eight hundred images upload with interruption - [ ] Derivative generation survives a restart - [ ] Failures are reported per image - [ ] Progress reflects processing, not just upload ### Watch out - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. ## M2 · A gallery that is fast on a phone A client scrolls hundreds of images without waiting. ### Steps 1. Serve per-viewport sizes Full resolution scaled in the browser is why galleries feel broken. Files: `src/serve.ts` 2. Preserve profile and orientation A photographer's colours are the product. Files: `src/derivatives.ts` 3. Measure on a real device A desktop connection hides everything that matters here. Files: `tests/performance.md` ### Done when - [ ] Eight hundred images scroll smoothly on a phone - [ ] Sizes match the viewport - [ ] Colour profile and orientation are preserved - [ ] Time to first images is measured ### Watch out - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. ## M3 · Who can see it, and for how long Gallery access is controlled without making it awkward for the client. ### Steps 1. Sign image addresses too Protecting only the page leaves the photographs public. Files: `src/access.ts` 2. Set an expiry by default Galleries accumulate and storage costs money indefinitely. Files: `src/galleries.ts` 3. Expect the link to be shared It is how families see the photographs. Design for it. Files: `src/access.ts` ### Done when - [ ] Image addresses expire with gallery access - [ ] A password gates images as well as the page - [ ] Expiry is set by default - [ ] Link sharing works as intended ### Watch out - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. ## M4 · Favourites, per person Two family members can each pick their favourites without overwriting each other. ### Steps 1. Key selections to a per-gallery visitor Not to the gallery. The second person overwrites otherwise. Files: `src/selections.ts` 2. Persist without an account Clients will not sign up to pick photographs. Files: `src/visitors.ts` 3. Attribute and combine for the photographer Whose favourites these are is the useful part. Files: `src/review.ts` ### Done when - [ ] Two visitors' selections coexist - [ ] Selections persist without an account - [ ] The photographer sees attribution and a combined view - [ ] A visitor can name themselves ### Watch out - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. ## M5 · Downloads, watermarks and getting paid (production only) Clients get their files under the policy the photographer set. ### Steps 1. Stream bulk archives Building in memory fails at exactly the gallery sizes that matter. Files: `src/bulk.ts` 2. Apply watermark policy per gallery Previews yes, paid downloads never. Files: `src/watermark.ts` 3. Record downloads So a photographer can answer what a client already has. Files: `src/downloads.ts` ### Done when - [ ] A large bulk download streams without exhausting memory - [ ] Watermarks follow policy and never apply to paid downloads - [ ] Download policy is enforced server side - [ ] Downloads are recorded per photo ### Watch out - Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed. ===== OPERATIONS.md ===== # Operations · Pic-Time ## Backup Original files are a photographer's professional obligation and are irreplaceable. Back them up separately from the application, and treat derivatives as regenerable rather than backing them up at all. ## Restore Restore and confirm originals are intact and derivatives regenerate, since a restore that returns derivatives but loses originals has kept the disposable half. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Bandwidth per gallery and processing queue depth. Bandwidth is the cost that scales with success, and a queue that stops draining leaves a photographer with a delivery deadline and no gallery. ## Incident checklist A gallery unavailable when a client was told it would be there is the failure with professional consequences. Processing queue health is the leading indicator, and per-image failure reporting is what makes a partial delivery possible rather than a total one. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Eight hundred images are confirmed to upload with interruption and resume - [ ] A gallery is confirmed to scroll smoothly on a phone on mobile data - [ ] Image addresses are confirmed to expire with gallery access - [ ] Two visitors' selections are confirmed to coexist - [ ] A large bulk download is confirmed to stream without exhausting memory ## Launch constraint Do not market omitted Pic-Time capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Galleries, client references and selections. DATABASE_URL=postgres://localhost:5432/galleries # Required. Originals. Derivatives may live separately since they are regenerable. STORAGE_BUCKET=s3://gallery-originals # Required. Where signed derivative addresses are served from. CDN_BASE_URL=https://img.example.com # Required. Long enough to browse a gallery, short enough that a copied address expires. SIGNED_URL_TTL_MINUTES=60 # Required. Galleries accumulate and storage costs money indefinitely. GALLERY_DEFAULT_EXPIRY_DAYS=365 # Required. Present as a constant. A client who paid gets the file. WATERMARK_PAID_DOWNLOADS=never
# Pic-Time · indie build A client gallery platform where the engineering is delivering large images fast enough that a gallery of eight hundred photographs is browsable, and the discipline is that a photographer's delivery to a client is a professional commitment with a deadline attached. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Originals in object storage, derivatives generated once and cached | regenerating a preview per view is slow and expensive at gallery scale | | Delivery | A content network in front, with signed access per gallery | a gallery is browsed on a phone by people who will not wait | | Processing | A worker queue generating sizes on upload, resumable | eight hundred images is an hour of work and it will be interrupted | | Access | Per-gallery links with optional password and expiry | clients share the link, which is the point and also the risk | | Runtime | TypeScript with PostgreSQL | galleries, selections and orders are relational | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the processing workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Galleries, selections and orders with per-visitor keys. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Object storage with a content network** · storage is cheap; bandwidth is the real cost - Why: Delivering hundreds of images per gallery is a bandwidth problem before it is anything else, and serving directly from storage is slow and expensive. - Get it: A storage provider with a network in front, supporting signed access and direct uploads. - Verify: A signed image address works and stops working after expiry - [ ] **A real shoot of at least six hundred images** · free if you have one - Why: Upload resumption, processing time and gallery scrolling are all uninteresting below a few hundred images and become the whole problem above it. - Get it: A real set of full resolution files, including a mixture of orientations and at least one corrupt file. - Verify: Six hundred full resolution images are available including a deliberately corrupt one - [ ] **A phone on a mobile connection** · free - Why: Gallery performance is judged on a phone by a client who will not wait, and a desktop connection hides every problem that matters. - Get it: Any phone, tested away from a fast local network. - Verify: A gallery is browsed end to end on mobile data ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Print fulfilment and lab integration. - Image editing. - Studio management with contracts and scheduling. - Screenshot prevention. - gallery delivery, store automation, print labs, marketing, and support - proprietary raw-processing quality - cloud sync and sharing - large AI models - camera and print ecosystem If one of those is essential to you, that is the reason to keep paying for Pic-Time, and the README should say so rather than pretend.
# Build brief · Pic-Time The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me client photo galleries with proofing, downloads and delivery that does not fall over to replace Pic-Time. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Originals in object storage, derivatives generated once and cached · regenerating a preview per view is slow and expensive at gallery scale - Delivery: A content network in front, with signed access per gallery · a gallery is browsed on a phone by people who will not wait - Processing: A worker queue generating sizes on upload, resumable · eight hundred images is an hour of work and it will be interrupted - Access: Per-gallery links with optional password and expiry · clients share the link, which is the point and also the risk - Runtime: TypeScript with PostgreSQL · galleries, selections and orders are relational ### Data model (create this before Phase 1) - galleries(id, client_ref, title, shoot_date, access_kind, password_hash, expires_at, download_policy, watermark_policy) - photos(id, gallery_id, original_key, filename, captured_at, width, height, orientation, hidden) - derivatives(photo_id, size, key, bytes, generated_at) · generated once, cached, regenerable - selections(gallery_id, visitor_id, photo_id, kind, at) · kind is favourite, hidden or comment_anchor - comments(id, photo_id, visitor_id, body, at) - downloads(id, gallery_id, photo_id, visitor_id, size, at) · so a photographer knows what was taken - orders(id, gallery_id, items_json, total_minor, status, placed_at) · print orders where offered - A visitor identity is per gallery rather than global, because clients share links with family and each person's favourites should be their own. ### Phase 1 · Uploading a shoot without losing it Build: Direct-to-storage uploads with resumption, and a worker queue generating derivative sizes. Handle an interrupted upload session and a browser closed midway. Report progress honestly including failures. Trap: Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. Done when: Eight hundred images upload with the browser closed midway and resume without duplicates. Derivative generation runs in workers and survives a restart. Failed images are reported individually rather than failing the batch. Progress reflects actual completion rather than upload alone. Do not build yet: No gallery, access or selection. ### Phase 2 · A gallery that is fast on a phone Build: Serve appropriately sized derivatives per viewport with lazy loading. Preserve colour profile and orientation. Measure time to first images and scrolling smoothness on a real phone on a real connection. Trap: Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. Done when: A gallery of eight hundred images scrolls smoothly on a phone on a mobile connection, measured rather than assumed. Images are served at a size appropriate to the viewport. Colour profile and orientation are preserved so images look as intended. Time to first meaningful images is measured and stated. Do not build yet: No access control, selections or downloads yet. ### Phase 3 · Who can see it, and for how long Build: Per-gallery links with an unguessable token, optional password and an expiry. Signed access to derivatives so an image address is not permanently public. Handle a client sharing the link, which is expected. Trap: Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. Done when: Image addresses are signed and stop working when gallery access expires, tested by copying one. A password-protected gallery cannot be browsed without it including its images. Expiry is set by default rather than optional. Sharing the link works as intended, since that is what clients do. Do not build yet: No selections, downloads or orders yet. ### Phase 4 · Favourites, per person Build: Per-visitor selections within a gallery, with a durable identity requiring no account. Support comments anchored to photographs. Give the photographer a combined view showing who selected what. Trap: Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. Done when: Two visitors' selections coexist without overwriting, tested with two browsers. Selections persist across sessions without an account. The photographer sees selections attributed per visitor and combined. A visitor can name themselves so the photographer knows whose list is whose. Do not build yet: No downloads or orders yet. ### Phase 5 · Downloads, watermarks and getting paid Build: Per-gallery download policy by size, with optional watermarking on previews and never on purchased downloads. Package a bulk download without holding it in memory. Record what was downloaded. Trap: Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed. Done when: A bulk download of a large gallery streams without exhausting memory, tested at full size. Watermarks apply to previews per policy and never to paid downloads. Download policy is per gallery and enforced server side. Downloads are recorded so the photographer knows what the client took. Do not build yet: Nothing after this. ### Out of scope (and why) - Print fulfilment and lab integration, which is where the hosted products make their money and involves real logistics. - Image editing, which happens before delivery in a different tool entirely. - Studio management with contracts and scheduling, which is a separate product for the same customer. - Any serious attempt to prevent screenshots, which is not achievable and degrades the experience for paying clients. ### README must contain - That uploads go directly to storage and processing runs in resumable workers, because a big shoot will be interrupted - That image addresses are signed and expire with gallery access, not just the gallery page - That selections are keyed per visitor so two family members do not overwrite each other - That bulk downloads are streamed rather than built in memory - That watermarks apply to previews and never to paid downloads
# Agent instructions · Pic-Time indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Originals in object storage, derivatives generated once and cached, A content network in front, with signed access per gallery, A worker queue generating sizes on upload, resumable, Per-gallery links with optional password and expiry, TypeScript with PostgreSQL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity.
# Build plan · Pic-Time A client gallery platform where the engineering is delivering large images fast enough that a gallery of eight hundred photographs is browsable, and the discipline is that a photographer's delivery to a client is a professional commitment with a deadline attached. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Uploading a shoot without losing it Eight hundred images upload and process without the photographer babysitting it. ### Steps 1. Upload direct to storage, resumable Through the application server is how a big shoot fails. Files: `src/upload.ts` 2. Generate derivatives in workers Synchronous processing ties up the request and loses on restart. Files: `src/worker.ts` 3. Report per-image failures One corrupt file must not fail eight hundred. Files: `src/processing.ts` ### Done when - [ ] Eight hundred images upload with interruption - [ ] Derivative generation survives a restart - [ ] Failures are reported per image - [ ] Progress reflects processing, not just upload ### Watch out - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. ## Phase 2 · A gallery that is fast on a phone A client scrolls hundreds of images without waiting. ### Steps 1. Serve per-viewport sizes Full resolution scaled in the browser is why galleries feel broken. Files: `src/serve.ts` 2. Preserve profile and orientation A photographer's colours are the product. Files: `src/derivatives.ts` 3. Measure on a real device A desktop connection hides everything that matters here. Files: `tests/performance.md` ### Done when - [ ] Eight hundred images scroll smoothly on a phone - [ ] Sizes match the viewport - [ ] Colour profile and orientation are preserved - [ ] Time to first images is measured ### Watch out - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. ## Phase 3 · Who can see it, and for how long Gallery access is controlled without making it awkward for the client. ### Steps 1. Sign image addresses too Protecting only the page leaves the photographs public. Files: `src/access.ts` 2. Set an expiry by default Galleries accumulate and storage costs money indefinitely. Files: `src/galleries.ts` 3. Expect the link to be shared It is how families see the photographs. Design for it. Files: `src/access.ts` ### Done when - [ ] Image addresses expire with gallery access - [ ] A password gates images as well as the page - [ ] Expiry is set by default - [ ] Link sharing works as intended ### Watch out - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. ## Phase 4 · Favourites, per person Two family members can each pick their favourites without overwriting each other. ### Steps 1. Key selections to a per-gallery visitor Not to the gallery. The second person overwrites otherwise. Files: `src/selections.ts` 2. Persist without an account Clients will not sign up to pick photographs. Files: `src/visitors.ts` 3. Attribute and combine for the photographer Whose favourites these are is the useful part. Files: `src/review.ts` ### Done when - [ ] Two visitors' selections coexist - [ ] Selections persist without an account - [ ] The photographer sees attribution and a combined view - [ ] A visitor can name themselves ### Watch out - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. ## Not in this build - Print fulfilment and lab integration. - Image editing. - Studio management with contracts and scheduling. - Screenshot prevention. ## After v1, if you want it - Client proofing rounds with a stated deadline, which is the workflow photographers actually run - Storage lifecycle moving old galleries to cheaper tiers, since bandwidth and storage are the ongoing cost - A photographer-facing view of which galleries are approaching expiry
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Galleries, client references and selections. DATABASE_URL=postgres://localhost:5432/galleries # Required. Originals. Derivatives may live separately since they are regenerable. STORAGE_BUCKET=s3://gallery-originals # Required. Where signed derivative addresses are served from. CDN_BASE_URL=https://img.example.com # Required. Long enough to browse a gallery, short enough that a copied address expires. SIGNED_URL_TTL_MINUTES=60 # Required. Galleries accumulate and storage costs money indefinitely. GALLERY_DEFAULT_EXPIRY_DAYS=365 # Required. Present as a constant. A client who paid gets the file. WATERMARK_PAID_DOWNLOADS=never
# Pic-Time · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Pic-Time, host client galleries with favorites, downloads, and simple proofing. The hard boundary is gallery delivery, store automation, print labs, marketing, and support, plus image pipeline quality, models, and workflow polish. ## Product outcome Client galleries that stay fast on a phone with hundreds of images, where two family members can each pick their own favourites and a copied image address stops working when the gallery does. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop computer - local photo library - sufficient disk space - optional GPU for AI features ## Explicit non-goals for v1 - Print fulfilment and lab integration. - Image editing. - Studio management with contracts and scheduling. - Screenshot prevention. - gallery delivery, store automation, print labs, marketing, and support - proprietary raw-processing quality - cloud sync and sharing - large AI models - camera and print ecosystem ## Success criteria - Eight hundred images are confirmed to upload with interruption and resume - A gallery is confirmed to scroll smoothly on a phone on mobile data - Image addresses are confirmed to expire with gallery access - Two visitors' selections are confirmed to coexist - A large bulk download is confirmed to stream without exhausting memory
# Build brief · Pic-Time The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me client photo galleries with proofing, downloads and delivery that does not fall over to replace Pic-Time. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Originals in object storage, derivatives generated once and cached · regenerating a preview per view is slow and expensive at gallery scale - Delivery: A content network in front, with signed access per gallery · a gallery is browsed on a phone by people who will not wait - Processing: A worker queue generating sizes on upload, resumable · eight hundred images is an hour of work and it will be interrupted - Access: Per-gallery links with optional password and expiry · clients share the link, which is the point and also the risk - Runtime: TypeScript with PostgreSQL · galleries, selections and orders are relational ### Data model (create this before Phase 1) - galleries(id, client_ref, title, shoot_date, access_kind, password_hash, expires_at, download_policy, watermark_policy) - photos(id, gallery_id, original_key, filename, captured_at, width, height, orientation, hidden) - derivatives(photo_id, size, key, bytes, generated_at) · generated once, cached, regenerable - selections(gallery_id, visitor_id, photo_id, kind, at) · kind is favourite, hidden or comment_anchor - comments(id, photo_id, visitor_id, body, at) - downloads(id, gallery_id, photo_id, visitor_id, size, at) · so a photographer knows what was taken - orders(id, gallery_id, items_json, total_minor, status, placed_at) · print orders where offered - A visitor identity is per gallery rather than global, because clients share links with family and each person's favourites should be their own. ### Phase 1 · Uploading a shoot without losing it Build: Direct-to-storage uploads with resumption, and a worker queue generating derivative sizes. Handle an interrupted upload session and a browser closed midway. Report progress honestly including failures. Trap: Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. Done when: Eight hundred images upload with the browser closed midway and resume without duplicates. Derivative generation runs in workers and survives a restart. Failed images are reported individually rather than failing the batch. Progress reflects actual completion rather than upload alone. Do not build yet: No gallery, access or selection. ### Phase 2 · A gallery that is fast on a phone Build: Serve appropriately sized derivatives per viewport with lazy loading. Preserve colour profile and orientation. Measure time to first images and scrolling smoothness on a real phone on a real connection. Trap: Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. Done when: A gallery of eight hundred images scrolls smoothly on a phone on a mobile connection, measured rather than assumed. Images are served at a size appropriate to the viewport. Colour profile and orientation are preserved so images look as intended. Time to first meaningful images is measured and stated. Do not build yet: No access control, selections or downloads yet. ### Phase 3 · Who can see it, and for how long Build: Per-gallery links with an unguessable token, optional password and an expiry. Signed access to derivatives so an image address is not permanently public. Handle a client sharing the link, which is expected. Trap: Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. Done when: Image addresses are signed and stop working when gallery access expires, tested by copying one. A password-protected gallery cannot be browsed without it including its images. Expiry is set by default rather than optional. Sharing the link works as intended, since that is what clients do. Do not build yet: No selections, downloads or orders yet. ### Phase 4 · Favourites, per person Build: Per-visitor selections within a gallery, with a durable identity requiring no account. Support comments anchored to photographs. Give the photographer a combined view showing who selected what. Trap: Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. Done when: Two visitors' selections coexist without overwriting, tested with two browsers. Selections persist across sessions without an account. The photographer sees selections attributed per visitor and combined. A visitor can name themselves so the photographer knows whose list is whose. Do not build yet: No downloads or orders yet. ### Phase 5 · Downloads, watermarks and getting paid Build: Per-gallery download policy by size, with optional watermarking on previews and never on purchased downloads. Package a bulk download without holding it in memory. Record what was downloaded. Trap: Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed. Done when: A bulk download of a large gallery streams without exhausting memory, tested at full size. Watermarks apply to previews per policy and never to paid downloads. Download policy is per gallery and enforced server side. Downloads are recorded so the photographer knows what the client took. Do not build yet: Nothing after this. ### Out of scope (and why) - Print fulfilment and lab integration, which is where the hosted products make their money and involves real logistics. - Image editing, which happens before delivery in a different tool entirely. - Studio management with contracts and scheduling, which is a separate product for the same customer. - Any serious attempt to prevent screenshots, which is not achievable and degrades the experience for paying clients. ### README must contain - That uploads go directly to storage and processing runs in resumable workers, because a big shoot will be interrupted - That image addresses are signed and expire with gallery access, not just the gallery page - That selections are keyed per visitor so two family members do not overwrite each other - That bulk downloads are streamed rather than built in memory - That watermarks apply to previews and never to paid downloads
# Architecture · Pic-Time ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Originals in object storage, derivatives generated once and cached | regenerating a preview per view is slow and expensive at gallery scale | | Delivery | A content network in front, with signed access per gallery | a gallery is browsed on a phone by people who will not wait | | Processing | A worker queue generating sizes on upload, resumable | eight hundred images is an hour of work and it will be interrupted | | Access | Per-gallery links with optional password and expiry | clients share the link, which is the point and also the risk | | Runtime | TypeScript with PostgreSQL | galleries, selections and orders are relational | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Upload | Direct, resumable ingestion. | Any storage, provided uploads bypass the application server. | | Derivatives | Sizes generated once and cached. | Any processor, provided generation is asynchronous and resumable. | | Access | Signed gallery and image addresses. | Any scheme, provided image addresses expire with the gallery. | | Selections | Per-visitor favourites without accounts. | Any identity scheme, provided two visitors do not overwrite each other. | | Delivery | Streamed bulk downloads and watermark policy. | Any implementation, provided archives are streamed rather than buffered. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Galleries, client references and selections. - `STORAGE_BUCKET` · required · Originals. Derivatives may live separately since they are regenerable. - `CDN_BASE_URL` · required · Where signed derivative addresses are served from. - `SIGNED_URL_TTL_MINUTES` · required · Long enough to browse a gallery, short enough that a copied address expires. - `GALLERY_DEFAULT_EXPIRY_DAYS` · required · Galleries accumulate and storage costs money indefinitely. - `WATERMARK_PAID_DOWNLOADS` · required · Present as a constant. A client who paid gets the file. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Pic-Time product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Originals in object storage, derivatives generated once and cached, A content network in front, with signed access per gallery, A worker queue generating sizes on upload, resumable, Per-gallery links with optional password and expiry, TypeScript with PostgreSQL. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. - Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed.
# Delivery milestones · Pic-Time Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Uploading a shoot without losing it Eight hundred images upload and process without the photographer babysitting it. ### Steps 1. Upload direct to storage, resumable Through the application server is how a big shoot fails. Files: `src/upload.ts` 2. Generate derivatives in workers Synchronous processing ties up the request and loses on restart. Files: `src/worker.ts` 3. Report per-image failures One corrupt file must not fail eight hundred. Files: `src/processing.ts` ### Done when - [ ] Eight hundred images upload with interruption - [ ] Derivative generation survives a restart - [ ] Failures are reported per image - [ ] Progress reflects processing, not just upload ### Watch out - Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable. ## M2 · A gallery that is fast on a phone A client scrolls hundreds of images without waiting. ### Steps 1. Serve per-viewport sizes Full resolution scaled in the browser is why galleries feel broken. Files: `src/serve.ts` 2. Preserve profile and orientation A photographer's colours are the product. Files: `src/derivatives.ts` 3. Measure on a real device A desktop connection hides everything that matters here. Files: `tests/performance.md` ### Done when - [ ] Eight hundred images scroll smoothly on a phone - [ ] Sizes match the viewport - [ ] Colour profile and orientation are preserved - [ ] Time to first images is measured ### Watch out - Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection. ## M3 · Who can see it, and for how long Gallery access is controlled without making it awkward for the client. ### Steps 1. Sign image addresses too Protecting only the page leaves the photographs public. Files: `src/access.ts` 2. Set an expiry by default Galleries accumulate and storage costs money indefinitely. Files: `src/galleries.ts` 3. Expect the link to be shared It is how families see the photographs. Design for it. Files: `src/access.ts` ### Done when - [ ] Image addresses expire with gallery access - [ ] A password gates images as well as the page - [ ] Expiry is set by default - [ ] Link sharing works as intended ### Watch out - Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access. ## M4 · Favourites, per person Two family members can each pick their favourites without overwriting each other. ### Steps 1. Key selections to a per-gallery visitor Not to the gallery. The second person overwrites otherwise. Files: `src/selections.ts` 2. Persist without an account Clients will not sign up to pick photographs. Files: `src/visitors.ts` 3. Attribute and combine for the photographer Whose favourites these are is the useful part. Files: `src/review.ts` ### Done when - [ ] Two visitors' selections coexist - [ ] Selections persist without an account - [ ] The photographer sees attribution and a combined view - [ ] A visitor can name themselves ### Watch out - Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity. ## M5 · Downloads, watermarks and getting paid (production only) Clients get their files under the policy the photographer set. ### Steps 1. Stream bulk archives Building in memory fails at exactly the gallery sizes that matter. Files: `src/bulk.ts` 2. Apply watermark policy per gallery Previews yes, paid downloads never. Files: `src/watermark.ts` 3. Record downloads So a photographer can answer what a client already has. Files: `src/downloads.ts` ### Done when - [ ] A large bulk download streams without exhausting memory - [ ] Watermarks follow policy and never apply to paid downloads - [ ] Download policy is enforced server side - [ ] Downloads are recorded per photo ### Watch out - Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed.
# Operations · Pic-Time ## Backup Original files are a photographer's professional obligation and are irreplaceable. Back them up separately from the application, and treat derivatives as regenerable rather than backing them up at all. ## Restore Restore and confirm originals are intact and derivatives regenerate, since a restore that returns derivatives but loses originals has kept the disposable half. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Bandwidth per gallery and processing queue depth. Bandwidth is the cost that scales with success, and a queue that stops draining leaves a photographer with a delivery deadline and no gallery. ## Incident checklist A gallery unavailable when a client was told it would be there is the failure with professional consequences. Processing queue health is the leading indicator, and per-image failure reporting is what makes a partial delivery possible rather than a total one. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Eight hundred images are confirmed to upload with interruption and resume - [ ] A gallery is confirmed to scroll smoothly on a phone on mobile data - [ ] Image addresses are confirmed to expire with gallery access - [ ] Two visitors' selections are confirmed to coexist - [ ] A large bulk download is confirmed to stream without exhausting memory ## Launch constraint Do not market omitted Pic-Time capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Galleries, client references and selections. DATABASE_URL=postgres://localhost:5432/galleries # Required. Originals. Derivatives may live separately since they are regenerable. STORAGE_BUCKET=s3://gallery-originals # Required. Where signed derivative addresses are served from. CDN_BASE_URL=https://img.example.com # Required. Long enough to browse a gallery, short enough that a copied address expires. SIGNED_URL_TTL_MINUTES=60 # Required. Galleries accumulate and storage costs money indefinitely. GALLERY_DEFAULT_EXPIRY_DAYS=365 # Required. Present as a constant. A client who paid gets the file. WATERMARK_PAID_DOWNLOADS=never
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Pic-Time because photographers pay because image quality, catalog safety, and fast handling of huge libraries matter more than cloning sliders. The recurring cost buys raw codecs, color management, metadata, previews, face models, GPU support, storage, backups, sync, and export fidelity, not just the visible interface.
xgallery delivery, store automation, print labs, marketing, and support
xproprietary raw-processing quality
xcloud sync and sharing
xlarge AI models
xcamera and print ecosystem
Don't feel like building it? These folks already made it free.
PPiwigoPrivate client accounts, album permissions, favorites, downloads, and gallery themes on your own server; proofing works, commerce polish does not.open source↗no votes, no pay-to-list · just what's real
Pic-Time pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | 10 GB photo storage for the first 3 months, then 3 GB; about 3,000 photos initially; 1 GB video; unlimited galleries. |
| beginner | $8/workspace | $7/workspace | 20 GB photos (about 6,000) and 5 GB video (about 30 minutes); unlimited galleries. |
| professional | $25/workspace | $21/workspace | 100 GB photos (about 30,000) and 30 GB video (about 3 hours); unlimited galleries. |
| advanced | $50/workspace | $42/workspace | Unlimited photo storage and 60 GB video (about 6 hours); unlimited galleries. |
| agency | custom | — | Custom brands, users and capacity; 0 self-serve prices published. |
free tier10 GB photo storage for 3 months, then 3 GB; 1 GB video and unlimited galleries
billingmonthly + annual
hidden costsMulti-Brand costs $6/month for each additional brand; other commerce/fulfillment charges were not numerically disclosed on the live pricing page.
verified 2026-08-14 · source ↗
Vibecode Pic-Time
Kinda. The core of Pic-Time is buildable in a weekend with the prompt on this page, but there are real gaps: gallery delivery, store automation, print labs, marketing, and support, proprietary raw-processing quality. Read the honest list above before committing.
How much does Pic-Time cost?
Pic-Time costs about $8/month (Beginner, checked 2026-08-14), which is $96 per year.
What do I lose by replacing Pic-Time?
Honestly: gallery delivery, store automation, print labs, marketing, and support; proprietary raw-processing quality; cloud sync and sharing; large AI models; camera and print ecosystem. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Pic-Time?
Yes: Piwigo (Private client accounts, album permissions, favorites, downloads, and gallery themes on your own server; proofing works, commerce polish does not.) The prompt is for when you want it exactly your way.