Laravel 9 + Inertia.js + React 18 platform for live video streaming, creator memberships, VOD sales, and fan tipping. Streamers broadcast via RTMP (OBS/Larix); viewers watch HLS playback. Virtual token economy with four payment gateways.
- What is Stream-It?
- Tech Stack
- Architecture
- Database Schema
- Live Streaming
- Real-time Events (WebSockets)
- Token Economy
- Payment Gateways
- Private Streaming (1-on-1)
- User Roles
- Route Structure
- Admin Panel
- Getting Started
A creator monetization platform where:
- Streamers go live via RTMP (OBS / Larix Broadcaster), sell memberships, upload paid VOD content, and accept private 1-on-1 paid sessions
- Viewers buy token packs with real money, then spend tokens to tip, subscribe, purchase videos/galleries, and book private streams
- Platform takes a 25% commission on video sales, gallery sales, and private streaming; tips go 100% to the streamer
- Admin manages the full platform, processes payouts, configures payment gateways, and approves streamers
Backend:
| Package | Purpose |
|---|---|
Laravel 9 (^9.19) |
Core framework |
| Laravel Breeze | Email/password auth scaffolding |
| Laravel Sanctum 2.8 | API token auth |
Inertia.js Laravel 0.6.3 |
Server-side Inertia bridge |
| Tightenco Ziggy | Named routes in JavaScript |
agence104/livekit-server-sdk ^1.1 |
RTMP webhook handling (LiveKit/media server) |
ably/ably-php ^1.1 |
Ably broadcaster (alternative to Pusher) |
pusher/pusher-php-server ^7.2 |
Pusher broadcaster |
stripe/stripe-php ^10.11 |
Stripe Payment Intents |
overtrue/laravel-follow 5.1 |
Polymorphic follow system |
intervention/image 2.7 |
Profile/cover image processing |
mews/purifier 3.3 |
HTML sanitization |
league/flysystem-aws-s3-v3 |
S3 storage driver |
Frontend:
| Package | Purpose |
|---|---|
| React 18.2.0 | UI framework (via Inertia) |
@inertiajs/inertia-react 0.8.1 |
Inertia React adapter |
Vite 3.x + laravel-vite-plugin |
Build tool |
| Tailwind CSS 3.2 | Styling |
@headlessui/react 1.4 |
Accessible UI components |
video.js 8.0.4 |
HLS video player |
@stripe/react-stripe-js ^2.1.0 |
Stripe Payment Element |
pusher-js ^8.0.1 + laravel-echo |
WebSocket client |
react-toastify, react-icons |
Notifications + icons |
react-google-recaptcha-v3 |
reCAPTCHA on registration |
Streamer (OBS/Larix)
| RTMP broadcast
v
Media Server (RTMP ingest -> HLS transcode)
| |
| webhook | HLS segments (.m3u8)
v v
POST /api/streaming/ Viewer browser
validate-key Video.js HLS player
|
v
LiveKitController
set users.live_status = 'online'
broadcast LiveStreamStarted event
|
v
Pusher / Ably (Laravel Echo)
|
v
All viewers receive live update
Broadcasting driver: BROADCAST_DRIVER=pusher or ably. Frontend uses Pusher JS client with Laravel Echo. Both backends fully configured — switch via env var.
Note: Agora RTC SDK (agora-rtc-sdk-ng, agora-react-uikit) is in package.json but NOT implemented anywhere in the React components. Streaming uses RTMP + HLS only.
| Table | Key Columns |
|---|---|
users |
id, username, name, email, password, profile_picture, cover_picture, headline, about, tokens (wallet balance), is_streamer, is_streamer_verified, live_status (online/offline), is_admin, popularity |
user_meta |
user_id, meta_key, meta_value — stores streaming_key, thanks_message, payout_destination, payout_details |
tiers |
user_id (streamer), tier_name, perks, price (tokens/month), six_months_discount (%), one_year_discount (%) |
subscriptions |
tier_id, streamer_id, subscriber_id, subscription_date, subscription_expires, status (Active/Canceled), subscription_tokens |
token_packs |
name, tokens (int), price (double fiat) |
token_sales |
user_id, tokens, amount, gateway, status (paid/pending) |
tips |
user_id (tipper), streamer_id, tokens |
chats |
roomName, streamer_id, user_id, tip (nullable), message, chat_type |
private_streams |
streamer_id, user_id, tokens, stream_time, status (pending/accept/conform) |
streaming_prices |
streamer_id, token_amount, streamer_time_id |
streaming_times |
streamer_id, streaming_time (HH:MM) |
videos |
user_id, title, thumbnail, video (path), price (tokens), free_for_subs, views, category_id |
video_sales |
video_id, streamer_id, user_id, price (tokens after commission) |
galleries |
user_id, title, thumbnail, price, free_for_subs, views |
gallery_sales |
gallery_id, streamer_id, user_id, price |
commissions |
admin_id, streamer_id, video_id, type (Buy Videos / Private Streaming), tokens (admin's 25% cut) |
followables |
user_id, followable_type, followable_id, accepted_at (polymorphic) |
withdrawals |
user_id, tokens, amount, status (Pending/Paid/Canceled) |
room_bans |
streamer_id, user_id, ip |
banned |
ip (global IP bans) |
options_table |
option_name, option_value (key-value site config store) |
categories |
category, icon (streamer categories: Youtuber, Tiktoker, etc.) |
pages |
page_title, page_slug, page_content, page_type (page/post) |
notifications |
Laravel polymorphic notifications |
- Streamer's unique RTMP key stored in
user_meta(meta_key = 'streaming_key', format:{user_id}.{random16}) - Admin configures RTMP server URL in site settings (
options_table) - Streamer sets up OBS/Larix with: RTMP URL + their streaming key
Media server calls: POST /api/streaming/validate-key
?call=publish -> stream started
?call=publish_done -> stream ended
LiveKitController::validateKey():
publish:
-> Look up user by streaming_key in user_meta
-> Set users.live_status = 'online'
-> Broadcast: LiveStreamStarted (room-{username} channel)
-> Broadcast: LiveStreamRefresh (LiveStreamRefresh channel)
publish_done:
-> Set users.live_status = 'offline'
-> Delete ALL chat messages for this room
-> Broadcast: LiveStreamStopped
-> Broadcast: LiveStreamRefresh
- Video.js HLS player consuming
{APP_URL}/livestreams/{roomName}.m3u8 - Age gate: must be 18+ (checks
users.dob) to enter a live stream room - Token gate: must have tokens > 0 to send chat messages
| Event Class | Channel | Event Name | Payload |
|---|---|---|---|
LiveStreamStarted |
room-{username} (public) |
livestream.started |
channel |
LiveStreamStopped |
room-{username} (public) |
livestream.stopped |
channel |
LiveStreamRefresh |
LiveStreamRefresh (public) |
livestreams.refresh |
— |
LiveStreamBan |
room-{username} (public) |
livestream.ban |
— |
ChatMessageEvent |
room-{username} (public) |
livechat |
full Chat model |
LivePrivateStreamStarted |
room-{username} (public) |
private.livestream.started |
user, privateStream, chat |
Note: Chat messages are ephemeral — they are deleted when a stream ends (LiveStreamStopped triggers Chat::where('streamer_id', $id)->delete()).
Tokens are an internal virtual currency (users.tokens integer column). Exchange rate configured by admin in options_table (token_value).
| Source | Streamer Gets | Platform Gets |
|---|---|---|
| Tips in live chat | 100% | 0% |
| Video purchases | 75% | 25% (in commissions table) |
| Gallery purchases | 75% | 25% |
| Private streaming | 75% | 25% |
| Subscriptions | 100% | 0% |
- Tips: Any amount >= 1 token, instant transfer
- Subscriptions: Monthly / 6-month / yearly (discounts via tier settings)
- Videos: Per-video token price;
free_for_subs = yes= free for active subscribers - Galleries: Per-gallery price; free for subs optionally
- Private streams: Token amount per configured session duration
Admin defines token packs in token_packs table (e.g. Starter: 100 tokens for $90, Platinum: 5000 tokens for $3900). Purchasable via 4 gateways.
Streamers request payout (minimum threshold from min_withdraw option). Admin manually processes and marks as Paid. Payout destination stored in user_meta.
All four gateways handle token pack purchases only. Subscriptions/tips/video purchases use tokens (no direct fiat).
| Gateway | Flow | Integration |
|---|---|---|
| Stripe | React Stripe.js Payment Element -> PaymentIntent -> redirect to /stripe/order-complete |
stripe/stripe-php + @stripe/react-stripe-js |
| PayPal | IPN redirect flow -> POST /api/paypal/ipn validates with PayPal IPN server |
Direct cURL (no SDK) |
| CCBill | Server-side redirect to FlexForms with MD5 hash -> ANY /api/ccbill/webhooks |
MD5 hash verification, handles NewSaleSuccess event; supports USD, EUR, AUD, CAD, GBP, JPY |
| Bank Transfer | Manual: user uploads payment proof -> admin approves | Image upload + admin review |
1. Streamer configures packages: streaming_prices (tokens) + streaming_times (duration)
2. Viewer sends request: POST /api/chat/send-private-request
-> Tokens deducted from viewer IMMEDIATELY
-> Tokens credited to streamer IMMEDIATELY
-> private_streams record created (status = 'pending')
3. Streamer accepts: GET /api/chat/accept-streaming/{id}
-> status = 'accept'
-> Unique room created: private-{timestamp}_{username}
-> Broadcasts LivePrivateStreamStarted event
4. Session ends: POST /api/chat/finished-streaming-chat
-> status = 'conform'
-> 25% commission recorded in commissions table
-> Credits admin account with commission tokens
5. Cancel: triggers full token refund to viewer
Note: Stream visibility is controlled at the React level (checking chat_type and participant IDs), not at the media server level — same RTMP stream is used.
| Role | How Determined | Access |
|---|---|---|
| Visitor | Not authenticated | Browse channels, view stream pages |
| User | Authenticated, is_streamer = 'no' |
Buy tokens, tip, subscribe, purchase content, send private requests |
| Streamer | is_streamer = 'yes' + is_streamer_verified = 'yes' |
Go live, sell content, accept private sessions, request payouts |
| Admin | is_admin = 'yes' |
Full admin panel at /admin/* |
Streamer verification: User submits government ID image -> admin approves via admin panel -> sets is_streamer_verified = 'yes' -> sends StreamerVerifiedNotification email.
Web Routes (Inertia - React frontend):
GET / Homepage (live channels preview)
GET /channel/{user} Streamer profile
GET /channel/live-stream/{user} Live stream viewer (18+ + token gate)
GET /browse-channels/{category?} Browse all channels
GET /browse-videos VOD catalog
GET /browse-gallery Gallery catalog
GET /get-tokens Token purchase page
GET /membership/... Membership tier management (streamers)
GET /streamer/verify Verification submission form
GET /withdrawals Payout management
Payment Routes:
GET paypal/purchase/{pack} PayPal redirect
GET stripe/purchase/{pack} Stripe payment page
GET ccbill/purchase/{pack} CCBill redirect
GET bank-transfer/purchase/{pack}
API Routes:
POST /api/streaming/validate-key RTMP webhook (stream start/stop)
POST /api/streaming/update-status
GET /api/chat/latest-messages/{room}
POST /api/chat/send-message/{user}
POST /api/chat/send-private-request
GET /api/chat/accept-streaming/{id}
GET /api/follow/{user} Toggle follow
GET /api/search Channel/user search
POST /api/paypal/ipn PayPal webhook
ANY /api/ccbill/webhooks CCBill webhook
POST /api/upload-video-chunks Chunked video upload
Custom Blade-rendered panel at /admin/*:
- Dashboard stats
- User management (token balance, roles, ban/unban, impersonation)
- Streamer verification approval
- Payout processing
- Token pack CRUD
- Token sales log + manual grant
- Subscription management
- Video/gallery management
- Category management
- Payment gateway configuration (enable/disable Stripe/PayPal/CCBill/Bank)
- Site settings (title, SEO, token value, commission rate, min withdrawal)
- RTMP + chat + mail + cloud configuration
- Commission/earning reports
- Bulk email sender + tag pixels
composer install
npm install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed
php artisan serve
npm run devKey environment variables:
BROADCAST_DRIVER=pusher # or: ably
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=
RTMP_URL=rtmp://your-media-server/live
STRIPE_KEY=
STRIPE_SECRET=
CCBILL_SALT_KEY=
CCBILL_ACC_NO=
CCBILL_SUBACC_NO=
CCBILL_FLEX_FORM_ID=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=
AWS_BUCKET=
GOOGLE_RECAPTCHA_KEY=