An account that signs up through an invitation works on the inviter's
billing, so handing it a trial at signup spent its only trial before it
owned anything. The trial is now held back for collaborators and claimed
only explicitly: a Start Free Trial button on the new-workspace and
billing screens calls the new POST /api/billing/trial endpoint, which
grants the once-per-account trial atomically. Nothing starts the clock
as a side effect, and pure collaborators no longer see a trial-ending
banner about work that is not theirs.
Two reasons the number on the storage page could read as nothing.
The per-user Bunny figure was computed inside a two minute cache. The declared
size lands on the row in the same transaction that deletes the reservation, so
for up to two minutes an upload that had just succeeded counted as nothing:
usage fell back towards zero and the next upload was measured against a total
that ignored the one before it. The call to Bunny stays cached, because it is
the slow half and its answer is the same for everybody. The join against our own
rows is now read fresh, per user, on every check.
A failed call to Bunny returned an empty map before it had looked at a single
row, so an account with gigabytes of declared uploads read as empty whenever
Bunny was unreachable. Bunny's figure being gone is not a reason to forget the
sizes we wrote down ourselves.
The rule for which of the two numbers to charge is unchanged, and the comment
above it now says why rather than guessing. What Bunny reports mid-encode is
partial: storageSize counts what has been written so far and climbs as each
rendition lands. A six minute cut uploaded at 2.5 GB read as 475 MB halfway
through and settled above 3 GB once it finished, because Bunny keeps the
original alongside every rendition. Taking the larger of the declared size and
Bunny's is right at every point on that curve; taking Bunny's whenever it is
non-zero would hand most of the quota back in the middle of an encode.
The settings card also claimed a 200 GB limit while showing a 3 GB one, and told
a trial account to delete files or contact support.
The trial now starts inside the product, at email verification, and Stripe
grants none at all: checkout creates a subscription that bills immediately.
Verifying an address is what buys the seven days, which is also the cheapest
abuse control there is.
An unexpired trial is treated as an entitlement the account already holds, so a
Stripe sync can add access but never retracts a trial that has not run out. That
matters most for the abandoned checkout: the resulting incomplete subscription
carries no trial_end, and writing it through would have erased the days the
account still had and locked it out.
Unpaid accounts are bounded by what they can cost us rather than by what they
can do: one workspace, one project, 3 GiB of direct uploads. YouTube imports,
share links, guests, comments and approvals stay unlimited, because those are
the parts worth trying and they cost nothing. isPaidTier() is the new seam;
hasBillingAccess() answers a different question now that access no longer
implies a card.
Signup CTAs, the pricing card, the comparison pages, the terms and the refund
policy all said the trial converts to a paid plan by itself. It no longer does,
so they say what happens instead. Settings and a banner name both dates that
matter: when the trial ends, and the fifteen days after that during which
nothing is deleted.
/admin/growth compares the two funnels on signup to paid within a fixed 30 day
window, not trial to paid. Dropping the card requirement multiplies trials, so
the old ratio can fall while more people actually pay, and reading it that way
would retire the change for the wrong reason.
A Stripe customer can own several subscriptions. Two defects let that happen
and corrupt the user's billing state:
1. Checkout allowed a fresh subscription whenever the user was not ACTIVE/
TRIALING, so a PAST_DUE user started a brand-new subscription (Stripe
Checkout always creates one) instead of recovering the existing one.
Add hasRecoverableSubscription() (ACTIVE/TRIALING/PAST_DUE/UNPAID/
INCOMPLETE); block checkout and route these users to the billing portal
('Update Payment Method') both in the API guard and the settings UI.
2. Subscription webhooks trusted the event's single subscription, so an old
subscription's deletion could clobber a newer active one (marking the user
CANCELED / No access). Every subscription event now re-derives state from
the full set of the customer's Stripe subscriptions via
syncStripeCustomerSubscriptions() + selectAuthoritativeSubscription(),
making the sync order-independent and self-healing.
- Added storage quota enforcement for audio and image uploads in the respective routes.
- Introduced reservation system to manage concurrent uploads and prevent quota overages.
- Enhanced comment creation to account for audio and image attachment sizes against user quotas.
- Created new UploadReservation model to track in-flight upload reservations.
- Backfilled existing video assets with size information from R2.
- Added progress component for UI feedback during uploads.
- Updated API responses to include reservation IDs for better quota management.
- Adjusted error handling to return appropriate storage limit exceeded messages.
- Moved registration logic to a new client component for better separation of concerns.
- Integrated invite code requirement based on feature flags in the registration process.
- Enhanced dashboard functionality to conditionally enable Bunny uploads based on feature flags.
- Updated various components and API routes to check for Bunny uploads and Stripe billing feature flags.
- Added new feature flag utilities for managing feature toggles in the application.
- Added billing-related fields to the User model in the database.
- Implemented functions for managing billing access, including trial periods and subscription statuses.
- Created new billing utility functions for Stripe integration.
- Updated onboarding page to include billing overview and workspace creation eligibility.
- Enhanced route access checks to require billing access for certain actions.
- Implemented cleanup scripts for expired billing workspaces and associated media.
- Updated header component to conditionally show app navigation based on billing access.
- Added new migrations for billing-related database changes.
- Implement loading states for dashboard, project, and video views
- Introduce skeleton UIs for settings pages
- Add skeletons for Navbar and charts
- Enhance `Skeleton` component with a text variant
- Improve perceived performance during data fetching
- Update `.gitignore` with `Optimization.md`
- Removed Telegram setup instructions from settings page.
- Enhanced video and version comment APIs to include tag information.
- Added new CommentTag model in Prisma schema for managing tags.
- Created API routes for managing tags (GET, POST, PATCH, DELETE).
- Updated WatchPage to support tag selection and display.
- Introduced keyboard shortcuts modal for improved user experience.
- Added tag selection dropdown in comment input area.