
Libeflix is a movie streaming web application inspired by Netflix, built using the TALL stack (Tailwind CSS, Alpine.js, Laravel, and Livewire). The idea behind Libeflix was to create a platform that not only offers movie streaming but also provides a rich user experience with features such as:
- OAuth2 Authentication: Secure user login through popular platforms.
- Comprehensive Search Options: Search for movies by title, genre, credits, and more.
- Watch Later List: Users can save movies to watch at a later time.
- Popular Movies with Trailers: An updated list of trending movies with trailers and additional information.
- Top Movies: Display of top-rated movies.
- Recommended Movies: Personalized recommendations based on user preferences.
- Similar Movies: Suggestions of movies similar to the ones users have watched or liked.
- Streaming Availability: Stream movies directly if available.
- Social Interaction: Engage with other users in movie rooms.
- User-Friendly Display: Movies are showcased in a poster card style for a better browsing experience.
Libeflix is built using the TALL stack, which stands for Tailwind CSS, Alpine.js, Laravel, and Livewire. Each component of the stack plays a crucial role:
- Tailwind CSS: Provides a utility-first approach to styling, making it easy to create responsive and modern designs.
- Alpine.js: Offers a lightweight framework for adding interactivity to the frontend.
- Livewire: Facilitates reactive, dynamic interfaces using Laravel, without the need for a full JavaScript framework.
- Laravel: A powerful PHP framework used for building robust backend functionality and handling data.
Building Libeflix was a methodical process. Here are the key steps:
1. Wireframing:
- Create wireframes to plan the user interface and layout of the application.
- Use tools like Figma or Sketch to design the homepage, movie detail pages, and user profile pages.
- Iterate on the designs based on feedback to ensure a user-friendly interface.
Example Wireframe:


These wireframes outlines the structure and layout of the Libeflix UI, including key components such as the search bar, multiple option genre selection, featured movies carousel, popular movies section, and new releases section.
2. Initial Setup:
- Set up a new Laravel project to handle the backend logic and database interactions.
- Install and configure Tailwind CSS for a modern and responsive design.
- Install Livewire and Alpine.js to build reactive components and handle frontend interactions without full page reloads.
- Set up version control using Git and GitHub to manage code changes and collaboration.
Initial Laravel Setup
sshCopycomposer create-project --prefer-dist laravel/laravel libeflix
3. Authentication:
- Implement OAuth2 for secure user authentication, allowing users to log in through popular platforms like Google and Facebook.
- Configure Laravel Socialite for handling the OAuth2 authentication process.
Setting Up OAuth2
This code installs Laravel Socialite to our project:
sshCopycomposer require laravel/socialite
Configure Service Provider:
phpCopy// Add providers and aliases in config/app.php 'providers' => [ // Other service providers... Laravel\Socialite\SocialiteServiceProvider::class, ], 'aliases' => [ // Other aliases... 'Socialite' => Laravel\Socialite\Facades\Socialite::class, ],
4. Database Design:
- Design the database schema to store user information, movies, watch later lists, and social interactions.
- Use Laravel migrations to create the necessary tables and relationships.
Creating Migrations:
sshCopyphp artisan make:migration create_movies_table php artisan make:migration create_watch_later_table
"Movies" Schema Migration:
phpCopyuse Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('movies', function (Blueprint $table) { $table->id(); $table->string('original_name')->index(); $table->string('title')->index(); $table->mediumText('api_result')->index()->nullable(); //json $table->mediumText('alternative_titles')->index()->nullable(); //json $table->mediumText('keywords')->index()->nullable(); //json $table->string('disk')->default('libeflix'); $table->string('path'); $table->mediumText('trailers')->nullable(); //json $table->string('poster_path')->nullable(); $table->string('poster_path_url')->nullable(); $table->string('backdrop_path')->nullable(); $table->string('backdrop_path_url')->nullable(); $table->mediumText('genres')->index()->nullable(); //json $table->mediumText('credits')->index()->nullable(); //json $table->mediumText('images')->nullable(); //json $table->float('vote_average')->nullable(); $table->integer('runtime')->nullable(); $table->string('release_date')->nullable(); $table->string('tagline')->index()->nullable(); $table->text('overview')->index()->nullable(); $table->tinyInteger('is_free')->default(0); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('movies'); } };
"Watch Later Movies" Schema Migration:
phpCopyuse Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('movie_user', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('movie_id'); $table->timestamps(); $table->unique(['user_id', 'movie_id']); $table->foreign('user_id') ->references('id') ->on('users'); $table->foreign('movie_id') ->references('id') ->on('movies'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('movie_user'); } };
Run "Movies" and "Watch Later" migrations:
sshCopyphp artisan migrate
5. Building the UI:
- Use Tailwind CSS to style the application, ensuring it is responsive and visually appealing.
- Create a layout for the homepage, movie detail pages, and user profile pages.
- Design movie cards to display movie information in a visually appealing manner.
- Utilize CDNs (Content Delivery Networks) for faster loading times and better performance.
Example of a Blade Movie Poster Component:
componentCopy<!-- resources/views/livewire/movie-poster.blade.php --> <style> .fade-movie-poster { background: -moz-linear-gradient(top, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 1%, rgba(0,0,0,0.35) 45%, rgba(0,0,0,0.9) 100%); /* FF3.6-15 */ background: -webkit-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,0) 1%,rgba(0,0,0,0.35) 45%,rgba(0,0,0,0.9) 100%); /* Chrome10-25,Safari5.1-6 */ background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,0) 1%,rgba(0,0,0,0.35) 45%,rgba(0,0,0,0.9) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00000000', endColorstr='#e6000000',GradientType=0 ); /* IE6-9 */ } </style> <div class="flex relative cursor-default mr-4 overflow-hidden transition duration-500 ease-in-out rounded-lg opacity-75 group-hover:opacity-100"> <img src="https://image.tmdb.org/t/p/w780/f89U3ADr1oiB1s9GkdPOEpXUk5H.jpg" loading="lazy" class="m-auto w-full h-full absolute" alt=""> <div class="absolute w-full h-full fade-movie-poster p-2"> <div class="flex h-1/3 justify-between"> <!-- trailers icon --> <svg class="w-6 h-6 cursor-pointer rounded-md text-gray-200 p-0.5 fade-80 hover:bg-black hover:text-rose-500 transition duration-200 ease-in-out" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path> </svg> <!-- watch later icon --> <svg class="w-6 h-6 rounded-md text-gray-300 p-0.5 fade-80 cursor-pointer hover:bg-black hover:text-green-500 transition duration-300 ease-in-out" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.5 12.75l6 6 9-13.5"></path> </svg> </div> <div class="h-1/3 w-full flex"> <!-- play icon --> <svg class="w-13 h-13 cursor-pointer mx-auto text-gray-300 rounded-full fade-40 hover:text-gray-100 transition duration-500 ease-in-out" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> </svg> </div> <div class="flex h-1/3 p-1.5 items-end"> <div class="block w-full"> <div class="block text-center md:text-lg"> <div class="text-lg leading-none tracking-tight text-gray-200 capitalize font-staat xl:text-xl"> The Matrix </div> <div class="flex justify-center w-full mt-1 text-xs"> <div class="flex gap-x-0.5"> <div class="px-1 py-0.5 my-auto font-bold text-xs uppercase text-black leading-none bg-gray-400"> cc </div> <div class="px-1 py-0.5 my-auto font-semibold font-mono text-xs uppercase text-gray-300 leading-none bg-black border border-gray-500"> EN </div> <div class="px-1 py-0.5 my-auto font-semibold font-mono text-xs uppercase text-gray-300 leading-none bg-black border border-gray-500"> ES </div> <div class="px-1 py-0.5 my-auto font-semibold font-mono text-xs uppercase text-gray-300 leading-none bg-black border border-gray-500"> FR </div> <div class="px-1 py-0.5 my-auto font-semibold font-mono text-xs uppercase text-gray-300 leading-none bg-black border border-gray-500"> KR </div> </div> </div> <div class="text-sm leading-snug tracking-tight text-gray-400"> <div class="flex justify-center"> <span> 1999 </span> <x-bullet class="px-1" /> <span class="text-amber-500"> 82% </span> </div> </div> </div> </div> </div> </div> </div> </div>
This code snippet represents a lazy loaded movie poster component, with "add to watch later" , "trailer" icons, and linear gradient fading effect.
The movie poster component in Libeflix is a visually striking feature, seamlessly integrating essential details and captivating imagery. It includes a "trailers" icon, triggering a modal with available movie trailers, also indicates if captions are available in different languages and displays movie rating. This enhances user engagement. Following, I'll showcase a ribbon of random movie posters, illustrating its implementation and visual appeal:
Front-end Development:
With the design in place, I turned my attention to frontend development using the TALL stack. Leveraging Tailwind CSS and Blade components, I implemented the visual elements of Libeflix, focusing on creating a sleek and intuitive interface. Alpine.js enabled me to add dynamic behavior to the frontend, enhancing interactivity without compromising performance.


The following screenshots showcase the Libeflix application's responsive design, ensuring a seamless user experience across various devices
Libeflix's commitment to accessibility shines through in its responsive design, as demonstrated in these screenshots. From tablets to smartphones, the user experience remains seamless, ensuring users can enjoy their favorite movies on any device.
6. Movie Data Integration:
- Use the TMDB API to fetch movie details, trailers, and recommendations.
- Create services to handle API requests and map the data to the application’s models.
Fetching Movie Data - Popular Movies:
phpCopy$client = new \GuzzleHttp\Client(); $response = $client->get(env('TMDB_API_URL')), [ 'query' => [ 'api_key' => env('TMDB_API_KEY'), ], ]); $movies = json_decode($response->getBody()->getContents(), true);
Let's add some functionality to the Movie Model :
phpCopy// Models/Movie.php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Movie extends Model { use HasFactory; protected $guarded = []; protected $table = 'movies'; /** * Defines a many-to-many relationship * between movies and users indicating that multiple users * can add the movie to their 'watch later' list. * * @return BelongsToMany */ public function watchLaterUsers(): BelongsToMany { return $this->belongsToMany(User::class)->withPivot('created_at')->orderBy('movie_user.created_at', 'ASC'); } public function addWatchLater(User $user): User { return $this->watchLaterUsers()->save($user); } public function removeWatchLater(User $user): bool { return $this->watchLaterUsers()->detach($user); } }
watchLaterUsers():
This method defines a many-to-many relationship
between movies and users, indicating that multiple users can add the movie to their 'watch later'
list. The method returns the relationship, specifying the pivot table
and ordering the results by the creation date of the pivot records.
addWatchLater(User $user):
This method adds a user to the 'watch later'
list for the movie. It associates the user with the movie in the pivot table and returns the User
instance.
removeWatchLater(User $user):
This method removes a user from the 'watch later'
list for the movie. It detaches the user from the movie in the pivot table and returns a boolean indicating whether the operation was successful.
Overall, this Model encapsulates the logic for managing the "watch later" functionality, allowing users to add and remove movies from their "watch later" list.
The User Model :
phpCopy// Models/User.php /** * All 'watch later' movies from current user * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function watchLaterMovies(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { return $this->belongsToMany(Movie::class)->withPivot('created_at')->orderBy('movie_user.created_at', 'DESC'); }
The watchLaterMovies() method defined within the User model retrieves all movies added to the current user's 'watch later' list. This method establishes a many-to-many relationship between users and movies, allowing users to maintain a personalized list of films to watch at a later time. By sorting the results in descending order based on the creation date of the pivot records, the method ensures that the most recently added movies appear first in the list.
The following routes facilitate seamless navigation and interaction within the Libeflix application, ensuring secure access and efficient file streaming capabilities.
phpCopy// routes/web.php use App\Http\Controllers\LoginController; use App\Http\Controllers\StorageController; use App\Livewire\StartLibeflix; // OAuth2 routes Route::get('/login/google', [LoginController::class, 'redirectToGoogle'])->name('login.google'); Route::get('/login/google/redirect', [LoginController::class, 'handleGoogleCallback']); Route::middleware([ 'auth:sanctum', config('jetstream.auth_session'), 'verified', ])->group(function () { Route::get('/libeflix', StartLibeflix::class)->name('libeflix'); Route::get('/stream/{encodedFilePath}/{disk}', 'StorageController@fileServe')->name('stream'); // Additional routes... });
The /login/google/
route is responsible for initiating the OAuth2
authentication process with Google. It directs users to the redirectToGoogle
method within the LoginController
when accessed.
The /login/google/redirect/
route is used as a callback URL for handling the OAuth2
response from Google. It invokes the handleGoogleCallback
method within the LoginController
to complete the authentication process.
Within a middleware
group, several routes are protected by authentication middleware auth:sanctum
along with Jetstream's
session management config('jetstream.auth_session')
and user verification "verified"
. This ensures that only authenticated
and verified
users can access these routes.
The /libeflix/
route maps to the StartLibeflix
Livewire component, serving as the entry point to the Libeflix application.
The /stream/{encodedFilePath}/{disk}/
route is used for streaming files from Storage
. It directs requests to the fileServe
method within the StorageController
, allowing users to access and stream files stored on the server.
File serve controller for streaming to registered users:
phpCopy// Http/Controllers/StorageController.php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\BinaryFileResponse; class StorageController extends Controller { /** * Create a new MovieController instance. * Apply 'auth' middleware to ensure only authenticated users can access controller methods. * * @return void */ public function __construct() { $this->middleware('auth:sanctum'); } /** * Storage file-serve for streaming * * @param string $encodedFilePath * @param string $disk * @return BinaryFileResponse */ public function fileServe(string $encodedFilePath, string $disk): BinaryFileResponse { $filePath = \App\Service\Tool::decode($encodedFilePath); $prefix = config('filesystems.disks.' . $disk . '.root'); $finalFilePath = $prefix . '\\' . $filePath; if (!Storage::disk($disk)->exists($filePath)) abort(404); return request()->download ? response()->download($finalFilePath, $filePath) : response()->file($finalFilePath); } }
This code defines the StorageController , responsible for serving files from storage for streaming purposes. It ensures only authenticated users can access its methods. The fileServe method decodes the file path, retrieves the file from storage, and serves it as an API response, allowing users to download or stream files securely.
Fetch "Watch Later" movies from authenticated User :
phpCopy// Service/Libeflix.php /** * Get current user's 'watch later' movies list * * @return array */ public static function getWatchLaterMovies(): array { $watchLaterMovies = []; if (Auth::check()) { $watchLaterMovies = Auth::user()->watchLaterMovies->map(function ($movie) { return $movie->only('id', 'title', 'poster_path_url', 'vote_average', 'runtime', 'trailers', 'release_date')->toArray(); }); } return $watchLaterMovies; }
Make sure that your filesystems.php
configuration is pointing to the correct local phisical storage path where the streaming media is located.
This code defines a method within the Libeflix service
called getWatchLaterMovies()
, which retrieves the current user's 'watch later'
movie list. It checks if the user is authenticated
, retrieves the relevant movie details, and returns them as an array, including attributes like ID, title, poster path URL, vote average, runtime, trailers, and release date.
Integration and Testing:
As development progressed, I focused on integrating frontend and backend components to ensure seamless functionality. Rigorous testing, including unit tests and end-to-end testing, was conducted to identify and resolve any issues or bugs. Continuous integration and deployment pipelines were established to automate the build and deployment process, facilitating efficient iteration and updates.
phpCopy// tests/Feature/AddToWatchLaterListTest.php test('user can add movie to watch later list', function () { $user = User::factory()->create(); $movie = Movie::factory()->create(); $response = $this->actingAs($user)->post(`/watch-later/{$movie->id}`); $response->assertStatus(200); $this->assertDatabaseHas('watch_later', [ 'user_id' => $user->id, 'movie_id' => $movie->id, ]); });
This PEST test checks if a user can add a movie to their "watch later" list. It creates a user and a movie, simulates the user adding the movie to their list, and then verifies that the database has recorded this action correctly.
7. Interactive Features:
- Implement the search functionality to allow users to search for movies by title, genre, and other criteria.
- Add a "watch later" list feature to enable users to save movies for future viewing.
- Enable social interactions in movie rooms, allowing users to discuss movies and share their thoughts.
- Integrate feedback mechanisms using Slack for real-time user feedback and issue tracking.
Adding a Movie to the "Watch Later" list:
phpCopypublic function addToWatchLater($movieId) { auth()->user()->watchLater()->attach($movieId); session()->flash('message', 'Movie added to your watch later list.'); }
8. Real-Time Updates:
- Use Livewire to enable real-time updates without full page reloads (SPA - Single Page Application).
- Create components for dynamic features like updating watch later lists and real-time chat in movie rooms.
Livewire Component: Add to "watch later"
phpCopyclass WatchLater extends Livewire\Component { public $movieId; public function addToWatchLater() { auth()->user()->watchLater()->attach($this->movieId); $this->dispatch('movieAdded'); } public function render() { return view('livewire.watch-later'); } }
9. Analytics and Performance Monitoring:
- Integrate analytics tools like Google Analytics to monitor user behavior and application performance.
- Use monitoring tools to track application health and performance metrics.
10. Community and User Engagement:
- Implement features to foster community and user engagement, such as live chat during movie streams and personalized recommendations.
- Encourage user feedback and participation through social features and feedback mechanisms.
11. Testing and Deployment:
- Write unit and feature tests to ensure the application functions as expected.
- Deploy the application using a platform like Heroku or DigitalOcean, and configure the environment for production.
Running Tests:
sshCopyphp artisan test
Deploying to Heroku:
sshCopyheroku create libeflix git push heroku main heroku run php artisan migrate
The main problems I aimed to solve with Libeflix were enhancing the user experience and providing comprehensive features for movie streaming. Traditional movie streaming platforms often lack comprehensive search capabilities and social interaction features. My goal was to address these gaps by providing a robust search system, allowing users to save movies for later, and enabling social interactions within movie rooms. I wanted to create an engaging platform where users could easily find and enjoy content.
One major design decision was to use the TALL stack for its simplicity and efficiency in building dynamic applications. This stack allowed me to create a cohesive and maintainable codebase. The user interface was designed with a focus on simplicity and usability. The poster card style for displaying movies was chosen to make browsing visually appealing and intuitive.
I also decided to implement real-time features using Livewire and Alpine.js, which enabled dynamic interactions without the need for page reloads. This provided a smoother and more engaging user experience. Additionally, I prioritized responsive design to ensure the application works well on both desktop and mobile devices.
To solve these problems, I relied on:
- Laravel: For the backend logic, handling data storage, and processing API requests.
- Tailwind CSS: For creating a responsive and modern user interface.
- Alpine.js and Livewire: For building reactive components and managing state within the frontend.
- OAuth2: For secure and easy user authentication.
- TMDb API: For fetching comprehensive movie data, including details, trailers, and recommendations.
- Git and GitHub: For version control and collaboration.
- CDNs: For faster loading times and better performance.
- Analytics Tools: For monitoring user behavior and application performance.
- Slack: For real-time feedback and issue tracking.
In building Libeflix, I utilized a variety of packages to address specific challenges and enhance the functionality of the platform. Each package played a crucial role in solving key problems and improving the overall user experience. Here are the packages I used:
- laravel/sanctum is a Laravel package for API token authentication. It allowed me to secure Libeflix's API endpoints and authenticate users when accessing protected resources, ensuring that user data remains secure and protected from unauthorized access.
- james-heinrich/getid3 is a PHP library for analyzing and extracting metadata from media files. By using this package, I was able to retrieve detailed information about media files uploaded to Libeflix, such as duration, bitrate, and file format, which helped improve content organization and searchability.
- laravel-spotify is a Laravel package that provides an elegant and intuitive interface for interacting with the Spotify API. It allowed me to integrate Spotify's vast catalog of music and audio content seamlessly into Libeflix, enhancing the platform's media-related features and recommendations.
- laravel/jetstream is an official Laravel package that provides robust authentication scaffolding and user management features out of the box. It facilitated the implementation of user registration, login, and profile management functionality in Libeflix, saving time and effort in building these essential features from scratch.
- laravel/socialite is an official Laravel package for OAuth authentication with popular social media platforms such as Facebook, X (Twitter), and Google. By integrating laravel/socialite into Libeflix, I enabled users to sign up and log in using their social media accounts, simplifying the registration process and enhancing user convenience.
- pbmedia/laravel-ffmpeg is a Laravel package for interacting with FFmpeg, a powerful multimedia processing tool. It allowed me to perform various video and audio processing tasks within Libeflix, such as transcoding, resizing, and watermarking, enabling me to optimize media files for streaming and improve overall performance.
- spatie/laravel-permission is a Laravel package that provides a simple and flexible way to manage user permissions and roles. It facilitated role-based access control in Libeflix, allowing me to define granular permissions and restrict access to certain features or content based on user roles, enhancing security and privacy.
Throughout this project, I learned the intricacies of the TALL stack and how to integrate various technologies to build a cohesive application. I gained a deeper understanding of Laravel’s capabilities, especially in managing complex backend logic and API integrations. Working with Tailwind CSS improved my skills in creating responsive designs quickly.
Using Livewire and Alpine.js taught me how to create interactive and reactive user interfaces. I also learned the importance of thorough testing and iterative development to ensure a stable application. Additionally, I learned how to effectively use version control with Git and GitHub, and how to leverage CDNs for improved performance.
If I were to rebuild Libeflix, I would:
- Enhance Scalability: Focus on optimizing the application for scalability to handle a larger user base and more concurrent streams.
- Improve UI/UX: Continuously gather user feedback to refine the user interface and improve the user experience.
- Expand Features: Add more social features and community engagement tools to enhance user interaction.
- Better Analytics: Integrate more advanced analytics to gain deeper insights into user behavior and preferences.
Building Libeflix was a challenging yet rewarding experience that allowed me to grow as a web developer. By leveraging the TALL stack and various other technologies, I was able to create a feature-rich, responsive, and user-friendly movie streaming platform. This project taught me the importance of careful planning, continuous testing, and user feedback in the development process. I hope this journey inspires others to take on similar projects and explore the capabilities of the TALL stack.
I encourage anyone interested in web development to embark on their own projects, learn from the challenges, and continuously strive to improve their skills. The journey from concept to deployment is filled with valuable lessons and rewarding experiences.
