<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Digital</title>
    <description>The latest articles on DEV Community by Digital (@digital_c680616c89b2f3272).</description>
    <link>https://dev.to/digital_c680616c89b2f3272</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4173629%2F1e490992-f595-436a-8bb7-8f8e984e8b0a.png</url>
      <title>DEV Community: Digital</title>
      <link>https://dev.to/digital_c680616c89b2f3272</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/digital_c680616c89b2f3272"/>
    <language>en</language>
    <item>
      <title>Stop chasing client assets: a TypeScript state machine for content handoffs</title>
      <dc:creator>Digital</dc:creator>
      <pubDate>Tue, 20 Oct 2026 09:07:52 +0000</pubDate>
      <link>https://dev.to/digital_c680616c89b2f3272/stop-chasing-client-assets-a-typescript-state-machine-for-content-handoffs-pdj</link>
      <guid>https://dev.to/digital_c680616c89b2f3272/stop-chasing-client-assets-a-typescript-state-machine-for-content-handoffs-pdj</guid>
      <description>&lt;p&gt;A website project can have two backlogs: &lt;strong&gt;code to ship&lt;/strong&gt; and &lt;strong&gt;content the client still needs to provide&lt;/strong&gt;. Most project boards track the first one very carefully. The second backlog often lives in scattered emails, chat messages, shared folders, and an optimistic sentence: "I'll send the logo tomorrow."&lt;/p&gt;

&lt;p&gt;This is not simply a file-upload problem. It's a small workflow problem with states, permissions, versioning, and clear ownership.&lt;/p&gt;

&lt;p&gt;Here is a practical approach to designing that workflow. The code below is a &lt;strong&gt;reference design&lt;/strong&gt;, not a claim about the internals of any particular product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with requirements, not uploads
&lt;/h2&gt;

&lt;p&gt;Instead of asking a client to "send your content," define the things a project actually needs.&lt;/p&gt;

&lt;p&gt;For a small business website, that might mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Homepage headline and introduction&lt;/li&gt;
&lt;li&gt;Logo in a usable format&lt;/li&gt;
&lt;li&gt;Services descriptions&lt;/li&gt;
&lt;li&gt;Team photos with permission to publish&lt;/li&gt;
&lt;li&gt;Contact details and opening hours&lt;/li&gt;
&lt;li&gt;Legal text provided or approved by the responsible party&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each requirement should have a label, a short explanation, an owner, a due date if appropriate, and a status. A photo cannot be "done" merely because someone uploaded a blurry thumbnail.&lt;/p&gt;

&lt;p&gt;A minimal data model could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RequirementStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;requested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;changes_requested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Requirement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;guidance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequirementStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;dueAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;currentVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;approvedAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;approvedBy&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important distinction is &lt;strong&gt;received !== approved&lt;/strong&gt;. Upload success is a storage event; approval is a business decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Express approval as a state machine
&lt;/h2&gt;

&lt;p&gt;If you model statuses as arbitrary strings that anyone can overwrite, bugs appear quickly. What happens when a client uploads a new version &lt;em&gt;after&lt;/em&gt; approval? What if a background job overwrites a human review?&lt;/p&gt;

&lt;p&gt;Define explicit transitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;requested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;changes_requested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;upload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;request_changes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approve&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reopen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;request_changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;changes_requested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;changes_requested&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reopen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;requested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Actor&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;teamActions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;request_changes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approve&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reopen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teamActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reviewer permission required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;transitions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid transition: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; never trust an &lt;code&gt;actor&lt;/code&gt; value sent by the browser. In production, derive permissions from the authenticated session or a server-validated client-share grant. Apply the transition inside a transaction and check the expected version so two simultaneous actions cannot silently overwrite one another.&lt;/p&gt;

&lt;p&gt;This small state machine gives a developer something concrete to test. For example: can a client approve their own deliverable? No. Can a reviewer request changes after receiving a file? Yes. Can an approved asset be reopened intentionally? Yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat each upload as a version
&lt;/h2&gt;

&lt;p&gt;A requirement and its files are not the same entity.&lt;/p&gt;

&lt;p&gt;The requirement might be "Team photo." Its upload history could be &lt;code&gt;photo-1.jpg&lt;/code&gt;, then &lt;code&gt;photo-final.jpg&lt;/code&gt;, then &lt;code&gt;team-landscape.jpg&lt;/code&gt;. Keep immutable upload records, attach them to the requirement, and store which version is approved.&lt;/p&gt;

&lt;p&gt;For each upload, record a generated object key, file size, media type, upload time, checksum when useful, and who submitted it. Don't use the original filename as your storage key; filenames collide and may contain unsafe characters.&lt;/p&gt;

&lt;p&gt;When a new version arrives, move the requirement from &lt;code&gt;changes_requested&lt;/code&gt; to &lt;code&gt;received&lt;/code&gt; and make the new version the one awaiting review. Preserve the prior review history for context.&lt;/p&gt;

&lt;h2&gt;
  
  
  A client portal should minimize friction
&lt;/h2&gt;

&lt;p&gt;You usually don't need clients to learn a project-management suite just to send assets.&lt;/p&gt;

&lt;p&gt;A good client view is a focused page with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The project's required items.&lt;/li&gt;
&lt;li&gt;A short explanation and example for each item.&lt;/li&gt;
&lt;li&gt;An input appropriate to the item: text field or file picker.&lt;/li&gt;
&lt;li&gt;A visible status and correction comments.&lt;/li&gt;
&lt;li&gt;A clear indication that the submission worked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mobile matters: clients may already have their photos on a phone. Don't require account creation when a narrower, secure access mechanism can do the job.&lt;/p&gt;

&lt;p&gt;A shared URL, however, is &lt;strong&gt;not&lt;/strong&gt; permissionless magic. Use a random, unguessable token, store only a secure representation server-side where feasible, scope it to one project, set an expiry, support revocation, and rate-limit requests. Don't put sensitive personal data in the link itself.&lt;/p&gt;

&lt;p&gt;For file uploads, validate size and type server-side, isolate storage by project, scan content where appropriate, and only generate short-lived download URLs for authorized viewers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate reminders, not relationships
&lt;/h2&gt;

&lt;p&gt;A reminder job should query &lt;strong&gt;outstanding requirements&lt;/strong&gt;, not blast the same generic email to every client.&lt;/p&gt;

&lt;p&gt;A useful notification says exactly what remains missing or needs changes and provides a direct way to continue. Make the cadence predictable and easy to pause. If a client is blocked because they have not commissioned their team photos yet, another identical reminder will not solve it.&lt;/p&gt;

&lt;p&gt;A simple scheduled job can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for each active project:
  find outstanding requirements
  skip if all items are approved
  skip if a reminder was sent too recently
  send a concise summary of the remaining items
  record the reminder event for audit and rate limits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event log also helps answer "Did the client receive the request?" without relying on someone's memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define a finish line
&lt;/h2&gt;

&lt;p&gt;Metrics are useful only if they describe progress toward an actual handoff. Consider measuring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time from project kickoff to final content approval&lt;/li&gt;
&lt;li&gt;Share of uploads approved on first review&lt;/li&gt;
&lt;li&gt;Number of correction rounds&lt;/li&gt;
&lt;li&gt;Outstanding items by project&lt;/li&gt;
&lt;li&gt;Reminder count before completion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If files arrive quickly but require many corrections, improve the item guidance. If guidance is clear but clients can't finish submissions, examine the interface and access flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does this need its own tool?
&lt;/h2&gt;

&lt;p&gt;For one client, a well-structured document and shared folder might be enough. For an agency juggling multiple websites, a requirement-based workflow can save coordination overhead and avoid ambiguous handoffs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I'm working on &lt;a href="https://paradocs.fr/" rel="noopener noreferrer"&gt;ParaDocs&lt;/a&gt;, a tool focused on requesting, reviewing, and collecting client content through a simple client link. This article describes a general engineering approach; the sample code is illustrative and is not the source code of ParaDocs.&lt;/p&gt;

&lt;p&gt;I'd be interested to hear how other developers manage asset approval and version changes before launching client sites.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>freelance</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop Chasing Clients for Website Content: A Practical Intake Workflow</title>
      <dc:creator>Digital</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:35:37 +0000</pubDate>
      <link>https://dev.to/digital_c680616c89b2f3272/stop-chasing-clients-for-website-content-a-practical-intake-workflow-1549</link>
      <guid>https://dev.to/digital_c680616c89b2f3272/stop-chasing-clients-for-website-content-a-practical-intake-workflow-1549</guid>
      <description>&lt;p&gt;You can have a great design system, a clean component library, and a perfect deployment pipeline—and still miss your website launch date because a client has not sent their copy.&lt;/p&gt;

&lt;p&gt;This is not a technical bug. It is a &lt;strong&gt;content intake problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you build websites for clients, here is a simple process you can reuse to collect logos, text, photographs, and approvals without turning every project into an endless email thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a shared folder is not a complete intake process
&lt;/h2&gt;

&lt;p&gt;A folder answers &lt;strong&gt;where files live&lt;/strong&gt;. It does not tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which files are still missing.&lt;/li&gt;
&lt;li&gt;Which page each piece of copy belongs to.&lt;/li&gt;
&lt;li&gt;Whether an image is approved for publication.&lt;/li&gt;
&lt;li&gt;Who should provide or review an item.&lt;/li&gt;
&lt;li&gt;What needs correction before development can move forward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding another chat thread usually makes this worse. A better approach starts with structured requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Turn vague requests into individual deliverables
&lt;/h2&gt;

&lt;p&gt;Instead of asking for "all website content," create a short checklist that a busy client can understand.&lt;/p&gt;

&lt;p&gt;For a small services website, try:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Expected format&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brand&lt;/td&gt;
&lt;td&gt;Primary logo&lt;/td&gt;
&lt;td&gt;SVG, PNG, or high-resolution image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;Main headline and introduction&lt;/td&gt;
&lt;td&gt;Short text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Services&lt;/td&gt;
&lt;td&gt;Service names and descriptions&lt;/td&gt;
&lt;td&gt;Text per service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;About&lt;/td&gt;
&lt;td&gt;Company story and team information&lt;/td&gt;
&lt;td&gt;Text and optional photos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contact&lt;/td&gt;
&lt;td&gt;Public contact details and opening hours&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio&lt;/td&gt;
&lt;td&gt;Approved project photographs&lt;/td&gt;
&lt;td&gt;Image uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;Required legal/privacy information&lt;/td&gt;
&lt;td&gt;Client-approved text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep the requests concrete. "Write an About page" can be intimidating; "Tell us in 3–5 sentences what your company does and whom it helps" is easier to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Establish a single source of truth
&lt;/h2&gt;

&lt;p&gt;Choose one place for the client to submit information. It might be a project portal, form, or dedicated collection link.&lt;/p&gt;

&lt;p&gt;The goal is not to force the client to adopt your project-management stack. They should be able to click a link, understand the next task, and submit their content.&lt;/p&gt;

&lt;p&gt;For internal tracking, give each request a status:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requested&lt;/strong&gt; — still waiting for the item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Received&lt;/strong&gt; — provided but not yet reviewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revision needed&lt;/strong&gt; — there is a specific correction to make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approved&lt;/strong&gt; — ready to use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A collection process that makes missing items visible will be more useful than a random pile of uploads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Reduce the effort required from the client
&lt;/h2&gt;

&lt;p&gt;A few details make a big difference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use plain-language labels, not internal jargon.&lt;/li&gt;
&lt;li&gt;Explain what a good answer looks like.&lt;/li&gt;
&lt;li&gt;Let clients provide partial information and return later, where possible.&lt;/li&gt;
&lt;li&gt;Make the process work on mobile.&lt;/li&gt;
&lt;li&gt;Avoid unnecessary sign-ups when the client simply needs to upload a file or submit text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your client is probably not thinking about your sprint board. They are thinking about running their business.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Agree on a realistic submission deadline
&lt;/h2&gt;

&lt;p&gt;Content is a dependency. Treat it as part of the project timeline.&lt;/p&gt;

&lt;p&gt;At kickoff, identify the &lt;em&gt;minimum content&lt;/em&gt; needed to start design and the &lt;em&gt;final content&lt;/em&gt; needed before launch. Explain the consequences of missing each milestone.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please send the logo and primary page copy by Tuesday. We can begin layout work once those arrive. Remaining photos and final approvals can follow by Friday.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When following up, list only the missing pieces. It is easier to act on "We still need your logo and three service descriptions" than "Just checking in."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Keep review and approval separate from collection
&lt;/h2&gt;

&lt;p&gt;"Uploaded" does not mean "approved."&lt;/p&gt;

&lt;p&gt;A photo might lack permission for use, a service description might still be a draft, and contact details might be out of date. Record approvals explicitly rather than assuming that any submitted file is final.&lt;/p&gt;

&lt;p&gt;For regulated businesses, get the appropriate legal and privacy information reviewed by the responsible people; a collection checklist cannot replace legal advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  A message you can send to your next client
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Hi [Name],&lt;br&gt;&lt;br&gt;
To keep your website project moving, I prepared a simple checklist of the content we need.&lt;br&gt;&lt;br&gt;
Please use this link to provide the texts, photos, logo, and business details: [link].&lt;br&gt;&lt;br&gt;
You can start with what you already have. Our first content milestone is [date].&lt;br&gt;&lt;br&gt;
If any request is unclear, just reply here and I'll help.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The tool I'm building around this workflow
&lt;/h2&gt;

&lt;p&gt;I'm working on &lt;strong&gt;&lt;a href="https://paradocs.fr/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=content_intake_oct2026" rel="noopener noreferrer"&gt;ParaDocs&lt;/a&gt;&lt;/strong&gt;, a bilingual (French/English) tool for freelancers and web agencies. You prepare the content requests, send a single link, and keep track of what a client has submitted. Clients can provide text and files without creating an account.&lt;/p&gt;

&lt;p&gt;It offers a 14-day free trial. I mention it because I'm involved in the project, not as an independent recommendation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question for fellow developers and agency owners:&lt;/strong&gt; what is the most painful part of getting content from clients—collecting it, chasing missing items, or getting final approvals? I'd love to hear what workflow works for you.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>freelancing</category>
      <category>productivity</category>
      <category>saas</category>
    </item>
    <item>
      <title>I Built ParaDocs to Stop Chasing Clients for Files — and Added ChatGPT + MCP Support</title>
      <dc:creator>Digital</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:34:03 +0000</pubDate>
      <link>https://dev.to/digital_c680616c89b2f3272/i-built-paradocs-to-stop-chasing-clients-for-files-and-added-chatgpt-mcp-support-32md</link>
      <guid>https://dev.to/digital_c680616c89b2f3272/i-built-paradocs-to-stop-chasing-clients-for-files-and-added-chatgpt-mcp-support-32md</guid>
      <description>&lt;h1&gt;
  
  
  I Built ParaDocs to Stop Chasing Clients for Files — and Added ChatGPT + MCP Support
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;If you build websites or deliver client projects, you know the real bottleneck is not always code. Sometimes, it's getting the logo, text, photos, and documents you need to finish the work.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: the project is ready, but the content isn't
&lt;/h2&gt;

&lt;p&gt;You've agreed on the scope, set up the project, and started building. Then come the inevitable questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where's the final logo?&lt;/li&gt;
&lt;li&gt;Has the client sent the team photos?&lt;/li&gt;
&lt;li&gt;Is the homepage copy approved?&lt;/li&gt;
&lt;li&gt;Which documents are still missing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Email threads, chat messages, and scattered folders aren't a great system for answering these questions. They make progress harder to track and follow-ups easy to forget.&lt;/p&gt;

&lt;p&gt;I created &lt;strong&gt;&lt;a href="https://paradocs.fr/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=paradocs_launch" rel="noopener noreferrer"&gt;ParaDocs&lt;/a&gt;&lt;/strong&gt; to make this part of client work much simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  A straightforward workflow for collecting client content
&lt;/h2&gt;

&lt;p&gt;ParaDocs is designed for developers, freelancers, designers, agencies, and other professionals who need files and information from their clients.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a project&lt;/strong&gt; and list the content you need: copy, logos, photos, PDFs, and other materials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send the client a secure link.&lt;/strong&gt; They can open it on a phone or computer without creating an account or installing an app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track each request.&lt;/strong&gt; See what is missing, received, approved, or needs a correction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review and request changes&lt;/strong&gt; on specific items so the client knows exactly what to update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send reminders&lt;/strong&gt; instead of repeatedly writing follow-up emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download the project content as a ZIP&lt;/strong&gt; when everything is ready.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a typical website project, this means you can replace dozens of scattered messages with one organized place for content collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about the reminders?
&lt;/h2&gt;

&lt;p&gt;Chasing client content isn't the best use of your time. ParaDocs includes reminder scheduling, so the system can follow up on missing items while you focus on delivery.&lt;/p&gt;

&lt;p&gt;You stay in charge of your client relationship, but you don't need to manually remember every outstanding file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I added a ChatGPT plugin
&lt;/h2&gt;

&lt;p&gt;I also wanted to make the workflow accessible from tools developers already use.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;ParaDocs ChatGPT integration&lt;/strong&gt; provides operations for working with the account's client and project information. For example, a connected assistant can help you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List your clients and projects.&lt;/li&gt;
&lt;li&gt;Check which requested content items are still pending.&lt;/li&gt;
&lt;li&gt;Review project progress.&lt;/li&gt;
&lt;li&gt;Request a reminder for a project with missing items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of opening different screens for every status check, you can ask for the information in a conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  And why an MCP server?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; is a way to expose tools to compatible AI clients.&lt;/p&gt;

&lt;p&gt;ParaDocs includes an &lt;strong&gt;MCP server&lt;/strong&gt;, allowing compatible assistants to access supported client and project operations through a standardized tool interface. That opens the door to integrating content collection into a wider workflow instead of isolating it in a single dashboard.&lt;/p&gt;

&lt;p&gt;The point is not to add AI for its own sake. It's to reduce routine administrative work and give teams more ways to access the information they already need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on a real project
&lt;/h2&gt;

&lt;p&gt;If you currently use email threads, shared drives, chat messages, or checklists to collect content, I'd love for you to try ParaDocs and tell me what works and what doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ParaDocs offers a 14-day free trial, with no credit card required.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://paradocs.fr/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=paradocs_launch" rel="noopener noreferrer"&gt;&lt;strong&gt;Try ParaDocs for free →&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd also love to hear from fellow developers: &lt;strong&gt;How do you currently collect files and content from clients?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was prepared with AI writing assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
