TaskFlow is a small educational project-management application built to practice and demonstrate modern Laravel, Vue, and Nuxt development.
The project focuses on clean, understandable fundamentals such as authentication, API development, Eloquent relationships, validation, authorization, testing, and frontend API integration.
- Multi-method API authentication
- Laravel Sanctum API authentication
- JWT API authentication
- Laravel Passport OAuth2 authentication
- Token-based API authentication
- Logout and authentication state handling
- User-owned Projects
- User-owned Tasks
- Projects containing Tasks
- Comments on Tasks
- Task CRUD
- Project CRUD
- Task status and priority filtering
- Pagination
- Form Request validation
- API Resources
- Policies and resource authorization
- Protection against assigning Tasks to another user's Project
- JSON API error handling for common HTTP errors
- Frontend validation error display
- Nuxt/Vue task management UI
- Nuxt/Vue project management UI
- Basic responsive styling
- Interactive API documentation (Scramble + Scalar)
- Laravel feature tests
- Docker-based development environment
- PHP 8.5
- Laravel 13
- Laravel Sanctum
tymon/jwt-auth- Laravel Passport
- Scramble (API documentation)
- Eloquent ORM
- SQLite for local development and automated tests
- MySQL-compatible configuration for environments using MySQL
- Nuxt
- Vue
- TypeScript
- Vite
- Docker
- Docker Compose
- Nginx
- PHP-FPM
- Node.js
The application is split into a Laravel backend and a Nuxt frontend.
Browser
|
v
Nginx
|
+--------------------+
| |
v v
Nuxt / Vue Laravel API
|
v
Eloquent
|
v
SQLite / MySQL
Nginx exposes a single application entry point:
/api/* -> Laravel
/* -> Nuxt
The frontend does not expose its development port directly to the host.
The current domain relationships are:
User
├── hasMany Projects
├── hasMany Tasks
└── hasMany Comments
Project
├── belongsTo User
└── hasMany Tasks
Task
├── belongsTo User
├── belongsTo Project
└── hasMany Comments
Comment
├── belongsTo User
└── belongsTo Task
Conceptually:
User
├── Project A
│ ├── Task 1
│ │ ├── Comment
│ │ └── Comment
│ └── Task 2
│
└── Project B
└── Task 3
You need:
- Docker
- Docker Compose
- Git
No PHP, Composer, Node.js, or Nginx installation is required on the host because they run inside Docker containers.
Clone the repository and enter the project directory.
Copy the environment file:
cp .env.example .envBuild and start the application:
./bin/run.sh buildGenerate the Laravel application key:
./bin/artisan key:generateGenerate JWT_SECRET (only once if JWT_SECRET is empty):
./bin/artisan jwt:secretGenerate passport keys (only once if they don't exist in backend/storage):
./bin/artisan passport:keysDatabase directory ownership is handled automatically — bin/run.sh maps your host user/group into the containers, so the SQLite file created by migrate is owned by your user.
Run migrations:
./bin/artisan migrate --seedThe application is then available through the configured host.
The development setup currently uses:
http://localhost:8080
If this hostname is not configured on your machine, point it to 127.0.0.1 or adjust the frontend/Vite host configuration accordingly.
The project contains three application services.
Laravel + PHP-FPM.
taskflow-backend
The container runs PHP-FPM on port 9000.
Nuxt development server.
taskflow-frontend
The Nuxt server listens internally on port 3000.
Single public entry point.
taskflow-nginx
Nginx exposes the application through the configured APP_PORT, which defaults to 8080.
There is intentionally no database container in the current development environment.
The root .env file is injected into the backend container as environment variables (via Compose env_file).
Example development configuration:
APP_NAME=TaskFlow
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8080
APP_PORT=8080
DB_CONNECTION=sqlite
JWT_SECRET=JWT authentication requires JWT_SECRET.
If the local .env does not already contain a JWT secret, generate one with:
./bin/artisan jwt:secretThe local .env file is not committed to Git.
Use .env.example as the committed environment template.
APP_PORT controls the port exposed by the Nginx container on the host.
The default is:
APP_PORT=8080The application can be configured to use MySQL instead of SQLite:
DB_CONNECTION=mysql
DB_HOST=dbhost
DB_PORT=3306
DB_DATABASE=taskflow.app
DB_USERNAME=tester
DB_PASSWORD=testerThe current Docker setup does not include a MySQL container because the development environment uses SQLite.
SQLite is used for local development and automated tests because it keeps the project setup lightweight.
The SQLite database is located at:
backend/database/database.sqlite
Laravel migrations are used to create and evolve the database schema.
Run migrations with:
./bin/artisan migrateReset and seed the database with:
./bin/artisan migrate:fresh --seedThe repository contains a convenience wrapper:
bin/artisan
This allows Laravel Artisan commands to be executed without entering the backend container manually.
Examples:
./bin/artisan --version
./bin/artisan migrate
./bin/artisan migrate:fresh --seed
./bin/artisan route:list
./bin/artisan tinker
./bin/artisan testLaravel generator commands can also be run through the wrapper:
./bin/artisan make:model Project -m
./bin/artisan make:controller Api/ProjectController
./bin/artisan make:request StoreProjectRequestGenerated files are handled on the host so that newly created project files are owned by the user running the command rather than by the Docker container user.
TaskFlow demonstrates three authentication mechanisms:
- Sanctum — Laravel personal access tokens
- JWT —
tymon/jwt-auth - Passport — OAuth2 authorization server
Protected API routes use the auth.multi middleware. The authentication
method is selected with the X-Auth-Method request header.
Supported values:
sanctumjwtpassport
If X-Auth-Method is omitted, Sanctum is used by default.
Example:
GET /api/v1/tasks
Authorization: Bearer <token>
X-Auth-Method: jwtPassport uses OAuth2 keys stored in the backend storage directory:
backend/storage/oauth-private.key
backend/storage/oauth-public.key
These keys are environment-specific and should not be committed to the repository.
Passport OAuth clients are stored in the application's oauth_clients table.
For manual API testing with the password grant, create a password-grant OAuth client using the Passport Artisan command and keep its client ID and secret available for your requests.
The OAuth token endpoint is:
POST /api/v1/oauth/token
The endpoint accepts OAuth2 parameters such as:
{
"grant_type": "password",
"client_id": "<client-id>",
"client_secret": "<client-secret>",
"username": "john@example.com",
"password": "password123",
"scope": ""
}The returned access_token can then be used with:
Authorization: Bearer <access-token>
X-Auth-Method: passportThe project tests do not depend on a manually created OAuth client. Passport clients required by tests are created inside the test database.
POST /api/v1/register
Example request:
{
"name": "John Doe",
"email": "john@example.com",
"password": "password123",
"password_confirmation": "password123"
}POST /api/v1/login
Example request:
{
"email": "john@example.com",
"password": "password123"
}The API returns a Sanctum bearer token.
JWT login uses the dedicated endpoint:
POST /api/v1/login/jwt
Example request:
{
"email": "john@example.com",
"password": "password123"
}The API returns a JWT bearer token.
For Passport login, the frontend first requests the password-grant OAuth client:
GET /api/v1/oauth/client
and then exchanges the credentials at POST /api/v1/oauth/token.
Authenticated requests use:
Authorization: Bearer <token>
GET /api/v1/user
Requires authentication.
POST /api/v1/logout
Requires authentication. The behavior depends on the authentication method sent via X-Auth-Method:
sanctum— deletes the current Sanctum access tokenjwt— invalidates the current JWTpassport— revokes the current Passport access token
All protected API routes require a valid bearer token authenticated through the selected authentication method.
The TaskFlow API is versioned with the v1 path prefix — every endpoint, including the Passport OAuth2 server and its helper endpoints, lives under /api/v1/*.
GET /api/v1/tasks
POST /api/v1/tasks
GET /api/v1/tasks/{task}
PUT /api/v1/tasks/{task}
DELETE /api/v1/tasks/{task}
The task list supports filtering:
GET /api/v1/tasks?status=completed
GET /api/v1/tasks?priority=high
GET /api/v1/tasks?status=completed&priority=high
Pagination is provided by Laravel's paginator.
Example:
GET /api/v1/tasks?page=2
A Task belongs to a Project.
Example:
{
"project_id": 1,
"title": "Build login page",
"description": "Implement the login screen",
"status": "pending",
"priority": "high",
"due_date": "2026-09-10"
}The API verifies that the selected Project belongs to the authenticated user.
Clients cannot set user_id to control task ownership.
GET /api/v1/projects
POST /api/v1/projects
GET /api/v1/projects/{project}
PUT /api/v1/projects/{project}
DELETE /api/v1/projects/{project}
Projects are owned by their creating user.
Example:
{
"name": "Website Redesign",
"description": "Redesign the company website"
}Comments are associated with Tasks.
GET /api/v1/tasks/{task}/comments
POST /api/v1/tasks/{task}/comments
GET /api/v1/tasks/{task}/comments/{comment}
PUT /api/v1/tasks/{task}/comments/{comment}
DELETE /api/v1/tasks/{task}/comments/{comment}
Example:
{
"body": "Looks good to me."
}A user can only access comments through Tasks they are authorized to view.
The backend ships with interactive API documentation generated by Scramble and rendered with Scalar.
The interactive documentation is available at:
GET /api/docs
The raw OpenAPI 3.1 specification is served at:
GET /api/docs.openapi.json
The specification describes the full API:
- every endpoint is versioned under
v1— the server ishttp://localhost:8080and operation paths keep the full/api/v1prefix (e.g.POST /api/v1/register,POST /api/v1/oauth/token) - endpoints grouped into Authentication, OAuth2, Tasks, Projects, and Comments
- both authentication security schemes
Authenticated endpoints require two security schemes:
Authorization: Bearer <token>— HTTP bearer token (Sanctum, JWT, or Passport)X-Auth-Method: sanctum|jwt|passport— header selecting the authentication strategy; documented as a companion header, not an API key
The Passport token flow (POST /api/v1/oauth/token and /api/v1/oauth/token/refresh) is grouped under OAuth2. The unused authorization-code and device flows are excluded from the generated specification.
Endpoint sections are driven by controller PHPDoc summaries and the #[Group] attribute.
Laravel Form Requests are used for request validation.
Examples include:
- required fields
- string validation
- maximum lengths
- allowed status values
- allowed priority values
- date validation
- email validation
- unique email validation
- existing Project validation
- Project ownership validation
Validation failures return HTTP 422.
The application currently uses Polish Laravel validation messages in:
backend/lang/pl/validation.php
The Nuxt frontend displays field-specific validation errors returned by the API.
Authorization is handled using Laravel Policies.
Users can view, update, or delete Tasks only when they own the Task.
Users can view, update, or delete Projects only when they own the Project.
A particularly important authorization rule is that users cannot associate Tasks with Projects belonging to another user.
User A
├── Project A
└── Task A
User B
└── Project B
User A cannot create or move a Task to Project B.
This ownership rule is enforced on the backend rather than relying on frontend behavior.
The API returns JSON responses for common errors.
401
{
"success": false,
"message": "Unauthenticated."
}403
{
"success": false,
"message": "This action is unauthorized."
}404
{
"success": false,
"message": "Resource not found."
}This keeps the API behavior predictable for the Nuxt frontend.
The Nuxt application contains separate pages for authentication, Tasks, and Projects.
frontend/app/pages/login.vue
frontend/app/pages/tasks.vue
frontend/app/pages/projects.vue
Shared navigation is implemented as:
frontend/app/components/AppNav.vue
Shared Task-related TypeScript definitions are stored in:
frontend/app/types/task.ts
Global styling is stored in:
frontend/app/assets/css/main.css
The frontend stores the bearer token and the selected authentication method in browser localStorage.
The authentication composable is:
frontend/app/composables/useAuth.ts
It handles:
- storing the token and selected method
- retrieving the token and selected method
- removing the token and selected method
- logout
The login page (frontend/app/pages/login.vue) lets the user pick the authentication method (Sanctum, JWT, or Passport).
API requests centralize their headers through:
frontend/app/composables/useApi.ts
which sets Accept, X-Auth-Method, and the Authorization bearer header on every request.
A client plugin (frontend/app/plugins/auth-header.ts) restores the stored authentication method so subsequent requests keep using the same method.
Browser-only APIs are guarded so they are not accessed during Nuxt server-side rendering.
The Tasks page currently supports:
- loading authenticated Tasks
- creating Tasks
- editing Tasks
- deleting Tasks
- selecting a Project
- filtering by status
- filtering by priority
- pagination
- loading comments
- adding comments
- displaying validation errors
- basic authentication redirects
Task actions are separated from task information visually, with editing/deletion controls placed on the right side of a task row and comments kept with the task content.
The Projects page currently supports:
- loading Projects
- creating Projects
- editing Projects
- deleting Projects
- displaying validation errors
- navigation to Tasks
- logout
The frontend uses a deliberately minimal global CSS layer.
The styling focuses on:
- readable typography
- centered content
- full-width application navigation
- consistent forms
- consistent buttons
- simple cards/rows
- basic responsive behavior
- clear validation/error messages
No external component library is required.
Laravel feature tests cover the main API behavior.
Everything runs inside the Docker containers (PHP, Composer, Node, and the pinned package/tool versions are only available there — nothing needs to be installed on the host). Start the stack first if it isn't running:
./bin/run.sh startRun the complete test suite with:
./bin/artisan test./bin/artisan is just a wrapper for docker compose exec backend php artisan <args>, so the same command works by hand:
docker compose exec backend php artisan testThe tests use an in-memory SQLite database (DB_DATABASE=:memory:), so no migrations or database setup are required before running them.
Run a specific test class or method:
./bin/artisan test --filter=TaskApiTest
./bin/artisan test --filter=ProjectApiTest
./bin/artisan test --filter=CommentApiTest
./bin/artisan test --filter=MultiAuthTest
./bin/artisan test --filter=test_logout_invalidates_jwt_tokenOr run a single test file:
./bin/artisan test tests/Feature/MultiAuthTest.phpCheck backend code style with Laravel Pint (inside the container):
docker compose exec backend vendor/bin/pint --test # check only
docker compose exec backend vendor/bin/pint # fix filesThe frontend has no automated test suite; validate it with a production build:
docker compose exec frontend npm run buildGitHub Actions runs two workflows on pushes and pull requests that touch backend/**:
- Lint — runs Laravel Pint in check mode (
pint --test) - Test — runs PHPUnit with pcov coverage and enforces a minimum 62% coverage threshold
Both workflows use PHP 8.5 and run on ubuntu-latest.
Current test coverage includes:
- listing a user's Tasks
- creating Tasks
- viewing Tasks
- updating Tasks
- deleting Tasks
- authorization between users
- validation
- authentication
- Project ownership during Task creation
- Project ownership during Task updates
- listing Projects
- creating Projects
- viewing Projects
- updating Projects
- deleting Projects
- authorization between users
- validation
- listing comments
- creating comments
- task ownership authorization
- validation
- unauthenticated access
MultiAuthTest verifies authentication through all three supported mechanisms:
- Sanctum authentication with an explicit
X-Auth-Method: sanctumheader - JWT authentication with an explicit
X-Auth-Method: jwtheader - Passport authentication with an explicit
X-Auth-Method: passportheader - Sanctum as the default when
X-Auth-Methodis not provided 401 Unauthorizedfor an invalidX-Auth-Method- logout deletes the current Sanctum token
- logout invalidates the current JWT
- logout revokes the current Passport token
The Passport test creates its OAuth client and obtains an access token through the OAuth2 password grant, keeping the test independent of manually configured OAuth clients.
Tests use Laravel's RefreshDatabase and Sanctum's actingAs() helpers.
Relevant repository structure:
.
├── .github/
│ └── workflows/
│ ├── lint.yaml # GitHub Actions — Laravel Pint
│ └── test.yaml # GitHub Actions — PHPUnit + coverage
├── backend/ # Laravel API
│ ├── app/
│ │ ├── Http/
│ │ │ ├── Controllers/Api/
│ │ │ ├── Middleware/
│ │ │ ├── Requests/
│ │ │ └── Resources/
│ │ ├── Models/
│ │ └── Policies/
│ ├── config/
│ ├── database/ # migrations + database.sqlite
│ ├── lang/ # en, pl validation messages
│ ├── routes/api.php
│ └── tests/Feature/
│
├── frontend/ # Nuxt / Vue
│ └── app/
│ ├── assets/css/
│ ├── components/
│ ├── composables/
│ ├── middleware/
│ ├── pages/
│ ├── plugins/
│ └── types/
│
├── bin/
│ ├── artisan # docker compose wrapper for artisan
│ └── run.sh # docker compose wrapper
│
└── docker/
├── docker-compose.yml
├── configs/nginx/default.conf
├── image-backend/Dockerfile
└── image-frontend/Dockerfile
All Docker-related configuration is kept under the docker/ directory.
Task and Project ownership is derived from the authenticated user.
The frontend cannot choose a different user_id to transfer ownership.
Task and Project authorization rules are implemented with Laravel Policies rather than duplicated throughout controllers.
This keeps ownership rules centralized and reusable.
Request validation is kept out of controllers where practical.
This makes controllers easier to read and keeps validation rules close to the request being validated.
Laravel API Resources provide a consistent response structure and prevent exposing raw Eloquent models directly as the public API contract.
When assigning a Task to a Project, the Project is looked up through the authenticated user's Projects rather than simply checking that the Project ID exists.
This prevents a user from attaching data to another user's resources.
SQLite keeps local development and automated testing simple and fast.
The application is also configured so it can use MySQL when required.
The project deliberately does not introduce Repository or Service layers everywhere.
Abstractions should solve a real problem rather than exist only for structural purposes.
Database transactions are not used simply for demonstration purposes.
A transaction is appropriate when a single application operation performs multiple related writes that must succeed or fail together. The current CRUD operations do not require an artificial transaction.
SQLite supports transactions, so the development database does not prevent using them when a real use case appears.
Control the Docker application with bin/run.sh:
./bin/run.sh # start
./bin/run.sh build # rebuild
./bin/run.sh stop # stopThe Artisan Helper section above documents migration, route, test, and shell commands.
Possible next improvements include:
- Role-based access control
- Project member management
- More granular permissions
- Comment editing and deletion
- More comprehensive frontend tests
- Improved production deployment configuration
- Better confirmation flows for destructive actions
- More reusable frontend components
These are intentionally outside the scope of the current project.
This project is intended as a practical learning exercise for:
- Laravel API development
- Eloquent relationships
- Form Requests
- Policies and authorization
- API Resources
- Sanctum authentication
- Pagination and filtering
- Validation
- Feature testing
- Vue/Nuxt application development
- TypeScript
- API integration
- Docker-based development
- Frontend state management
- Separation between frontend and backend responsibilities
The implementation intentionally favors straightforward Laravel and Vue patterns that are easy to understand, test, and maintain.
This project was created as an educational project for learning Laravel, Vue, and Nuxt.