What shipped, including the mistake.
One film of the whole product on the home page, and one of the MCP server on its page
The home page switched between three short cuts written before half of the current product existed. They are replaced by one film, just over two minutes long, that covers planning, publishing, analytics, the watchlist, several channels, the free tools and the MCP server. The MCP page now opens with a forty second film of Claude doing the jobs its tool list describes. Neither autoplays, and neither downloads anything until you press play. Both are silent for now: the track they were cut to is not licensed to Runsheet, so the published copies carry no audio at all rather than carrying it without permission.
Ideas saved over MCP were stored and then shown nowhere
The MCP tool that saves an idea still filed it at the first stage of the four column board, which was removed on 21 September. The Ideas page, the count on Today and the runsheet's idea picker only read ideas saved as ready, so an idea an agent saved went into the database and appeared on no screen. It is saved as ready now, the same as an idea kept from a suggestion, it lands at the top of the saved list, and the tool's own description stops describing a board that no longer exists.
Every limit says whether it is counted per channel or shared across the account
Every number on the pricing page was written when an account meant one channel, so nothing said whether Network's 250 queued videos meant 250 in total or 250 on each channel. Both readings are defensible, only one was true, and the answer lived in whichever query happened to run. Each limit now declares which it is, by one rule: work scales with channels and cost does not. The queue, monthly publishes, watched channels, view history and how much of the catalogue is snapshotted are counted for each channel, because raising them costs nothing but rows in a database. Drafting runs, thumbnail exports, AI-designed thumbnails and manual syncs are shared across the account, because each one spends money or YouTube quota that does not grow with the number of channels. The pricing page marks the per channel lines on every plan that covers more than one channel, which is the only place the two readings differ.
Creator covers two channels
Creator connected one channel, which left the channel count as the only thing Network could do that Creator could not do at all. A second channel is the most common real need one step below an agency, and holding it back until the $69 plan pushed those people to run it by hand. Creator covers two now and Network still covers five. The number is enforced rather than printed: every read is capped to the plan's count, connecting one past it is refused with the name of the plan that allows it, and a channel left over the cap by a downgrade is marked as over cap in the switcher rather than hidden. On a plan with room for another, the account menu and the first-run checklist now say so and offer to add it, because until then nothing on screen told a Creator customer that channels were plural.
Network is on sale: up to five channels, each with its own runsheet
Network covers two to five channels on one account, and each keeps its own runsheet, library and findings, with no number added across them. It waited for two things. The first was the multi-channel work recorded below, which made a second channel safe to read at all. The second was arithmetic: the AI budget at the time allowed 169 drafting runs a day across the whole application, and Network promises 500 to a single account, so its first customer using what they bought would have tripped the limit for everybody. The budget's base was raised to cover that with no revenue at all before the plan was listed, and a test now fails if any plan on sale promises more than that floor can pay for.
One account can now run several channels, and the failure it had to fix was a silent one
The database has allowed a second channel since the first migration: channels has been keyed on the account and the YouTube id, videos and both stats tables carry a channel, and tokens are stored per channel. What did not exist was an answer to which channel a screen is about, and the two ways that was missing failed very differently. Fourteen reads asked for the account's channel with a query that does not return the first of two rows, it errors, so those screens would have said no channel connected. Twenty-six reads filtered videos by the account instead, which does not error at all: it quietly adds two channels together and shows one Library, one runsheet and one set of findings, with nothing on the screen saying so. For a product whose argument is that its numbers are measured rather than invented, that is the worse of the two by a long way, and it would have shipped looking like it worked. All twenty-six are scoped to a channel now, and a new check in the build fails if a read of the video table arrives without one. The channel is chosen in the rail, the choice survives a new browser, and Today deliberately still spans every channel with each line labelled, because a broken token on a second channel must not be able to hide behind a first channel that is fine.
Naming one channel's public page wrote that name onto every channel on the account
The dossier address was saved with a query scoped to the account rather than to the channel, so it set the same address on every channel row. That address is unique across the whole product, so the second row collided with the first row of the same account and the screen reported the address was taken, by a channel that was also yours. It could not be reached while an account had one channel, which is why it survived to be found by the work that allowed a second.
A video deleted in YouTube Studio stayed on the runsheet forever
Sync only ever added and updated. A scheduled video deleted from YouTube therefore kept its place in the queue, and pressing Sync Channel confirmed it, because nothing in the product ever asked whether a video it was holding still existed. It asks now, by video id rather than by absence: the catalogue fetch stops at the plan's depth, so treating a missing video as a deleted one would have emptied a large channel. Four refusals come before the question is asked at all, including a ten minute grace so a video uploading right now cannot be mistaken for one that was removed.
Benchmarks becomes the Watchlist, and keeps the videos it was already fetching
A tracked channel used to be five aggregates: subscribers, video count, uploads a week, median views, last upload. None of them says what a channel makes, which is the only reason to open a screen about somebody else's channel. It was never a data problem. Every sync already pulled that channel's last fifty uploads and ran a second call across them to work out one median, then discarded the other forty-nine rows. They are kept now, and each watched channel has a page: which of its videos reached two and a half times the median of its own format, which days it posts on, its cadence, its shorts and long cut split, and how its length bands compare. All of it computed by the same module that reads your own catalogue, with the same sample thresholds and the same refusals, because a comparison against a number worked out a different way tells you nothing. One section is a model reading their titles to name recurring formats, and it is labelled as the only part of the page that is not measured. Their figures are held for thirty days and refreshed nightly, which is what the YouTube Developer Policies allow for a channel that did not authorise us.
Watched channels were never refreshed by anything on a schedule
The endpoint that refreshes them by hand carried a comment reading "the cron refreshes these anyway", and nothing in the cron had ever touched one. A tracked channel therefore held the numbers it was created with unless somebody pressed refresh, with a last-synced timestamp quietly going stale and no screen saying so. Twelve of the most overdue are now refreshed every hour. It stopped being optional with this release: the stored uploads are deleted after thirty days without a refresh, by the retention rule the Developer Policies require, so without the job a watchlist would have emptied itself out after a month.
Analytics and Insights were one question split across two rail items
One screen drew the curves and the other drew the conclusions, which asked a creator to know that "is it growing" and "what should I do about it" were two different destinations. Nobody has that question in two parts. The split also let the analytics screen be nothing but view counts, because everything interesting lived one click away on a page most people never opened. They are one screen. Two findings arrived with the merge, both from columns the database has held since the first migration and no screen had ever drawn: how strongly people react, as likes plus comments per thousand views with quartiles, and whether video length moves anything, in minute bands rather than equal-width buckets. The old address redirects rather than disappearing.
Ideas keeps a shortlist
Six suggestions arrive at once and one or two are usually worth keeping, but the button for six more was the same one that threw away the two you liked along with the four you did not. Each suggestion now has Save, asking for more is a button of its own, and what you save sits in a list underneath that asking for more cannot reach. Each suggestion also carries a short description of what the video would contain, what is shown, in what order and where it ends, because a title and a one-line angle are a prompt rather than a plan. Saved ideas go back into the table the old board used, so nothing typed into that board was lost, and they show up again on the runsheet's empty days.
Ideas asked you to type, when typing was never the hard part
It was a four column stage board: raw, scripted, filmed, ready. That is a good backlog for planning videos you have not made, and Runsheet is for somebody who already has a channel and is deciding what to do next week. For them it was a second place to type. The input is the channel now: the last thirty videos actually published go in, and six concrete suggestions come back, each naming which existing video it follows from. Nothing is stored, because a suggestion is used in the next thirty seconds or it is noise, and the only way out of the screen is onto the runsheet with a date attached. The old table is untouched: dropping it would destroy whatever anybody had typed into the board, and that is not a trade this product makes to tidy a screen.
The upload ceiling stopped being six a day in December and nobody told the code
YouTube changed the shape of its daily allowance twice and this product was built against the old one. An upload used to cost 1,600 units out of a shared 10,000, which put the whole application at six uploads a day; in December that cost fell to about 100, and in June uploads moved out of the shared pool into their own allowance of a hundred a day. The upload ceiling is therefore sixteen times what an error message in here was telling people, and reads no longer compete with publishes. There is also a limit Google does not document, which arrives as a different HTTP status from the documented one and was being passed through as a raw error; it is now recognised and explained. Refusals say when the allowance returns in hours rather than naming a timezone most people do not live in.
A shared allowance that refuses in English before Google refuses in JSON
Every customer's publishing comes out of one Google project, and nothing was rationing it. Per-account limits stop one person being greedy and do nothing about fifty people each being reasonable on the same day, so the first sign of trouble would have been Google refusing somebody mid-publish. There is now an application-wide ceiling set below Google's, which means the first refusal is ours and is written for a person, the gap absorbs the undocumented limit, and the consumption is visible on the health endpoint before it matters. A channel connected with its own Google client skips all of it, because it is not spending the shared allowance. The per-account number is sized so that batching a month of shorts in one evening still works, which is the thing this product is for.
A plan can be pencilled in, filled, and told apart from a real upload
Uploading a video you had already planned created a second row and left the plan behind, nothing in the product could create a plan in the first place, and a plan wore the same 'scheduled' chip as a video actually queued with YouTube. All three are fixed: you can pencil a slot in without a file, the composer opens on it with everything prefilled and editable, uploading fills that row rather than duplicating it, and the board now names every state including planned and failed. Filling a plan no longer counts against the queue twice, which used to refuse people at their cap for trying to do the thing the cap was telling them to do. Videos with no file also stop showing a day-by-day view of a journey that has not happened.
A client secret rode in a URL, and Google's errors were read out to strangers
The advanced screen for connecting your own Google client submitted by GET, which put the client secret in the address bar, the browser history, the Referer of the next page and the server access logs. It was then base64-encoded into the OAuth state, which travels to Google as a query parameter and comes back the same way. The form now posts and the credentials are encrypted with the same cipher as the refresh token. Separately, five routes were returning raw database and Google error text to whoever could make them fail; they now log the cause with a reference and answer with a sentence. The message shown when a channel stops syncing is the one exception and gets the opposite treatment: the causes that actually happen are translated into something to act on.
The board bucketed videos by UTC, so half a runsheet sat a day early
Days were keyed on the UTC date, so for anyone east of Greenwich an evening publish appeared in the wrong column: in India, everything after half past five in the afternoon was filed under tomorrow. The board now buckets by your own calendar day, and does it in a way that cannot flicker between what the server rendered and what your browser knows. Today also stopped telling creators that a slot they had missed had probably already published, thumbnails now appear on the board and the video page, and an upload that was started and abandoned no longer occupies a queue slot for ever.
Pages nothing linked to, and two claims a reader could act on and be wrong
The AI thumbnail designer had a route, a sitemap entry and no link from anywhere, so the one tool with a lifetime free cap was reachable only by typing its address; it now has a section on the tools index. It also inherited the writing tools' questions, which promised a few a day when a free account gets two ever, and said nothing you type is stored when the picture it draws is kept on purpose. The cadence planner's only button sent signed-in customers to the signup form. The sort parameter on a link from Analytics was never read, so the link silently showed a different ordering from the one it promised. The privacy policy predated the entire AI surface and now names what goes to Gemini, what is stored, and the IP addresses recorded for anonymous thumbnail exports, which are also now deleted after two days instead of kept for ever.
Five tables that any signed-in account could write to
Supabase grants every table in the public schema to signed-in users by default, and row level security then decides which rows. Five tables had policies written as 'for all', which includes UPDATE, so the columns the product's own limits are read from were writable from the browser: an account could set its own plan and expiry and take every paid limit, including the image allowance that costs real money per press. Four other tables in the same schema had always carried an explicit revoke, so this was a gap rather than a misunderstanding. Grants are now an allow list of the eight writes the app actually makes. Two service-role reads that loaded a channel by an id from a row the client could write are scoped to the owner as well, and the YouTube OAuth callback now checks the session instead of trusting only the signed state, which closes a way to attach somebody else's channel to your own account. Nothing indicates any of it was used.
Claims on this site that the code contradicted
A full audit of every page against the code found about fifteen. The about page advertised a nine dollar plan that has never existed. Three pages said there was no upload cap while a monthly publishing allowance was enforced as a hard refusal. The MCP page and llms.txt both said there were no write tools, months after four shipped. The methodology page repeated the exact 'paid plans sync four times a day' claim that the pricing page boasts about having retracted. A feature page advertised a year of history, which is a plan nobody can buy. Every one of them is now derived from the same module the server reads, or deleted. The worked example on the landing page was the worst of it: the numbers were typed rather than computed, and the box demonstrating a finding that refuses was showing a refusal the demo channel does not actually produce.
Google sign-in, password reset, and a way out of a new account
Sign in and sign up were a form on an empty page, with no password recovery anywhere in the product and, on a brand new account with no channel connected, no way to sign out at all. There are now three routes to the exit, a reset flow that gives the same answer whether or not an address is registered, and Google sign-in, which matters because everybody who can use this product has a Google account by definition. Google also finished verifying the branding, so the consent screen names Runsheet and shows the logo instead of the bare domain.
Buying a second plan charged you for both
Upgrading opened a new subscription without closing the old one, and only the newer of the two could be cancelled from Settings. Checkout now refuses when a live subscription already exists and says what to do instead. Two other refusals were wrong in the other direction: a lapsed account was told it had published far past its allowance when it had published nothing, and the CSV export paywall could never name a plan that would fix it, so it rendered with no button.
A four tier ladder, no trial, and buttons that actually buy
Pricing went from one paid plan to Starter and Creator, with a monthly and annual toggle and the seven day trial removed. Pressing a price now opens a checkout rather than dropping you on a signup form to find the plan again later. Every number on the page, in the comparison table, in the structured data and in llms.txt comes from one module, so the page cannot advertise something the server does not enforce.
Cue: writing tools where a video is actually written
Six drafting tools existed behind a Tools section and on the video editor, which meant the product offered to write you a title at the one moment a title is hardest to change. They now sit on the composer and on the ideas board, next to the field they fill, and nothing is ever filled automatically. A model writes copy here and never produces a finding: every number reported about a channel is measured, and no model is anywhere near that path.
Thumbnails, drag to reorder, and the MCP toolkit
A thumbnail maker that sets type over both shapes from one design and needs no account, and an AI designer that draws the whole thing. The runsheet board can be reordered by dragging, by pointer or by keyboard, with an announcement for screen readers. And a remote MCP server, so Claude or Cursor can work against your own channel with a key you issue, scope and revoke.
The spend breaker permitted $271 a month against a stated budget of $50
The per-call cost it protected the budget with was a constant written by hand, and the prices moved. It is derived now, and the tests assert relationships between the constants rather than their values, so the next price change cannot quietly widen the ceiling. Security headers were added, which this app had shipped none of, and database errors stopped being returned to the browser.
The app fits a phone, and a gate so it stays that way
The navigation rail is 248 pixels and a phone is 390, so the product was a crushed column overflowing off the right edge. The rail folds into a drawer, five layouts that could not shrink were rebuilt, and a script now fails the build on an ad-hoc font size, a hardcoded colour or a fixed width wider than a phone.
Every buyer in the world was charged the Indian price
The checkout did not pass the buyer's country, so purchasing power parity applied the cheapest tier of pricing to everybody. It reads the country at the edge now.
A live demo, as a sandbox per visitor
A pool of pre-seeded accounts, one claimed per visitor, with a real channel of 25 published videos and a fortnight of daily snapshots. It deletes itself after twelve hours. Nothing in it is shared, so there is nothing to break for the next person.
Features, comparisons, guides, methodology and a free cadence planner
The site went from three pages to twenty. Six feature pages, six head-to-head comparisons each with a section arguing for the other product, five guides, a methodology page publishing every sample-size threshold the insights module enforces, and a cadence planner that needs no account. Plus dynamic social images, structured data on every page, and error and not-found screens in the product's own language.
Search engine surface
robots.txt names the AI crawlers explicitly rather than leaving them to a wildcard, the sitemap includes public channel pages and survives a database failure, and every description now fits in a result snippet instead of being cut mid-sentence.
The runsheet board and the public channel page
Four weeks of the channel as a running order with the empty days visible, and an optional public page proving your cadence at an address you choose. Off by default.
A row-level policy that would have exposed encrypted YouTube tokens, added and reverted
While building the public channel page I added a policy allowing anyone to read a channel row when that page was public. That table holds encrypted access and refresh tokens, so the policy would have handed every opted-in creator's YouTube credentials to anyone with the public key. It was caught and reverted in the same session, before any page was public. It now reads through the service role with its columns named explicitly, and the table stays owner-only.
Video detail, benchmarks, and the insights maths
Per-video view curves instead of lifetime totals, channel benchmarks compared by cadence rather than subscriber count, and the findings module that refuses to answer below a stated sample size.
First release: connect, schedule, publish, measure
Google connect with all three YouTube scopes, browser-to-YouTube resumable upload so no video file ever touches our servers, scheduling handed to YouTube's own publishAt, daily stat snapshots on a database cron, and billing.
Known and open right now
The list a product this young usually keeps private. It is here because you are deciding whether to connect a channel to it.
- Google has verified the branding, so the consent screen now names Runsheet and shows the logo, but it has not finished reviewing the YouTube permissions themselves. Until it does, connecting a channel shows an unverified-app warning and the app is capped at 100 connected channels. Signing in with Google is unaffected and shows no warning, because it asks for nothing sensitive. You can also connect with your own Google client to remove the cap.
- A published video's metadata can be repaired from Runsheet, but its publish time cannot be changed. YouTube treats a publish time as a request to hold the video private, so moving one would take a live video down.