Build Thinkific
KINDAreplaces $54/mosaves $648/yrback to the verdict
A course delivery platform where the engineering is video at scale and the discipline is entitlement: what a student bought, for how long, and what happens to their progress when a payment fails, a course is updated, or you decide to stop selling it.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the server and the webhook handlers.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree locally
Why Entitlements, progress and payments with real constraints.
Get it Docker or your package manager. open ↗
Verify
psql connects and creates a table - accountusage based; a course with real students costs real money
Why Encoding, adaptive streaming and delivery are a substantial project, and signed short-lived URLs are the part that matters for access control.
Get it A provider supporting per-viewer signed playback URLs. Check the bandwidth pricing before uploading a course.
Verify
A video plays through a signed URL and the URL stops working after expiry - accountfree to test; per-transaction in production
Why Subscriptions, failed renewals and chargebacks all behave differently from a single charge and only a real provider shows you how.
Get it Sign up, use test keys, configure a webhook endpoint, and deliberately trigger a failed renewal in test mode. open ↗
Verify
A purchase, a subscription renewal and a failed renewal all produce the right entitlement state - decidefree
Why What a refund, a chargeback and a retirement do to access are promises to students, and deciding them after someone asks is deciding them badly.
Get it Write down what lifetime means, what a refund removes, how long a grace period lasts, and what happens if you stop selling a course.
Verify
A written policy covers refund, chargeback, renewal failure and retirement
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- courses(id, title, published_version_id, status) - course_versions(id, course_id, structure_json, published_at) · content changes and students bought a version - lessons(id, course_version_id, ordinal, kind, video_asset_id, body, duration_seconds, drip_offset_days) - students(id, email, created_at) - entitlements(id, student_id, course_id, kind, granted_at, expires_at, revoked_at, source_payment_id) · kind is lifetime, subscription or trial - progress(student_id, lesson_key, completed_at, seconds_watched) · keyed on a stable lesson key across versions - payments(id, student_id, kind, amount_minor, provider_ref, status, at) - Progress is keyed on a stable lesson key rather than a version-specific id, so updating a course does not reset everyone.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/courses | Students, entitlements and progress. |
VIDEO_PROVIDER_KEYsecret | required | a provider key | Uploads and signs playback URLs. |
PLAYBACK_URL_TTL_SECONDS | required | 300 | Short enough that a shared link is useless, long enough not to interrupt playback. |
RENEWAL_GRACE_DAYS | required | 7 | An expired card is not a cancellation. |
RETIRE_PRESERVES_ACCESS | required | always | Present as a constant. Lifetime access means after you stop selling too. |
STRIPE_WEBHOOK_SECRETsecret | required | whsec_... | Entitlements are granted from webhooks, so an unverified endpoint grants access. |
The build, in order
Video that plays and cannot be trivially taken
Lessons stream reliably and a playback link is not a permanent copy.
Self-hosting video for a course is a project of its own.
Files
src/video.tsIt stops casual sharing, which is what actually happens.
Files
src/playback.tsIt costs a lot, hurts legitimate students, and stops nobody determined.
Files
docs/piracy-position.md
done when · tick each as it passeswatch out- Building digital rights management to stop copying costs enormous effort, degrades playback for legitimate students, and does not stop anyone determined, since a screen recorder exists. Use short-lived signed URLs to stop casual link sharing, watermark with the student identity if you must, and spend the remaining effort on the course.
Entitlement as the product
What a student may access is a record, not a side effect of a payment row.
Access is the product. A payment is how it was acquired.
Files
src/entitlements.tsSupport cases and refunds both need it, and both should be recorded.
Files
src/entitlements.tsA student with two reasons to have access should have access.
Files
src/access.tssrc/access.test.ts
done when · tick each as it passeswatch out- Deriving access from the payment record means a refund, a chargeback or a failed renewal removes access instantly and sometimes wrongly, and a student who paid a year ago loses a course they own because a payment row changed state. Make the entitlement the thing, granted by a payment but with its own lifetime.
Payments, including when they fail
Purchases and subscriptions work, and a failure does not lose a student.
A student who pays and closes the tab has still paid.
Files
src/webhooks.tsAn expired card is not a cancellation.
Files
src/renewals.tsDifferent causes, different responses, different records.
Files
src/payments.ts
done when · tick each as it passeswatch out- Revoking access the instant a renewal fails locks out a student whose card simply expired, at the moment they were about to fix it, which converts a payment problem into a cancellation. Grace the failure, tell them, and revoke only after the grace period.
Progress that survives the course changing
Updating a course does not reset anyone's progress.
Version-specific ids reset everyone on republish.
Files
src/progress.tstests/republish.test.tsEnrolled students should see improvements, not a reset.
Files
src/versions.tsBoth happen. Both need an answer before the first update.
Files
src/progress.ts
done when · tick each as it passeswatch out- Keying progress on lesson identifiers that change when the course is republished resets every student to zero, and they will not tell you, they will simply not come back. Use a stable key that survives editing, and decide explicitly what happens when a lesson is split or removed.
Drip, completion and closing down gracefullyproduct builder
Content releases on schedule, completion is recorded, and students are not stranded.
Days since enrolment, not a server-side clock.
Files
src/drip.tsLifetime access means lifetime, including after you stop selling.
Files
src/courses.tsA certificate nobody can check is a picture.
Files
src/certificates.ts
done when · tick each as it passeswatch out- Retiring a course by unpublishing it removes access for everyone who bought lifetime access, which is exactly the promise you sold them. Separate stopping sales from removing access, and make retirement default to preserving existing entitlements.
That is the whole plan for Thinkific. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Digital rights management.
- Live cohorts and community features.
- Marketing funnels, affiliates and upsells.
- Cross-jurisdiction tax handling.
- course builder, payments, communities, apps, integrations, and support
- marketplace discovery
- tax remittance
- fraud operations
- advanced course and community tooling
- Quizzes with attempt limits, which is where completion becomes meaningful
- Bundles and multi-course entitlements, once the single-course model is proven
- A student-facing receipt and access history, which removes most support questions
Need the files? The project pack on the verdict page hands your agent the whole brief · more creator commerce.