id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); // Public, unpredictable identifier. Used in the watch URL AND as the // media path, so it must never be guessable or enumerable — knowing // one stream's id must not hint at another's. $table->string('public_id', 32)->unique(); $table->string('title'); $table->text('description')->nullable(); // idle → live → ended, or failed. $table->string('status', 16)->default('idle'); // public = listed on the live index // unlisted = reachable only with the link $table->string('visibility', 16)->default('public'); // Short-lived, single-use credential the browser presents to the // media server when it starts publishing. Not a long-lived stream // key: it is minted on "Go live" and expires in minutes, so there // is no durable secret to leak, screenshot, or reuse. $table->string('publish_token', 64)->nullable(); $table->timestamp('publish_token_expires_at')->nullable(); $table->timestamp('started_at')->nullable(); $table->timestamp('ended_at')->nullable(); // Last time the media server confirmed the publisher is still // connected — how a stream that died without saying goodbye gets // reaped. $table->timestamp('last_seen_at')->nullable(); $table->unsignedInteger('peak_viewers')->default(0); $table->unsignedInteger('current_viewers')->default(0); // Set once a finished stream has been turned into a normal video. $table->foreignId('video_id')->nullable()->constrained()->nullOnDelete(); $table->timestamps(); // The live index reads "public streams that are live, newest first". $table->index(['status', 'visibility']); $table->index(['user_id', 'status']); }); } public function down(): void { Schema::dropIfExists('livestreams'); } };