A test-driven, mobile API-first Laravel backend for a Google Calendar clone application. Features asynchronous email invitations, RESTful API with Sanctum authentication, and minimal Livewire admin dashboard.
- β Mobile API-First Design: RESTful API optimized for mobile apps
- β Asynchronous Email Processing: Queued invitation emails (non-blocking)
- β Test-Driven Development: Comprehensive feature tests included
- β Sanctum Authentication: Token-based API authentication
- β Google OAuth Integration: Social login via Laravel Socialite
- β Authorization Policies: Secure resource access control
- β Spatie Query Builder: Advanced filtering and sorting
- β Event Invitations: Support for registered and unregistered users
- β Admin Dashboard: Minimal Livewire CRUD interface
- PHP 8.1+
- Composer
- MySQL/PostgreSQL
- Laravel 10+
- Node.js & NPM (for Livewire admin)
git clone <repository-url>
cd Calendros
composer install
npm installcp .env.example .env
php artisan key:generateUpdate .env with your configuration:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=calendros
DB_USERNAME=root
DB_PASSWORD=
# Queue Configuration
QUEUE_CONNECTION=database
# Mail Configuration (development)
MAIL_MAILER=log
MAIL_FROM_ADDRESS=[email protected]
MAIL_FROM_NAME="Calendros"
# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URI=http://localhost:8000/api/auth/google/callback
# Sanctum
SANCTUM_STATEFUL_DOMAINS=localhost:8000
SESSION_DRIVER=cookiecomposer require laravel/sanctum
composer require laravel/socialite
composer require spatie/laravel-permission
composer require spatie/laravel-query-builder# Run migrations
php artisan migrate
# Create queue jobs table
php artisan queue:table
php artisan migrate
# Seed sample data (optional)
php artisan db:seednpm run buildThe application follows Test-Driven Development (TDD). All features have corresponding tests.
# Run all tests
php artisan test
# Run specific test suites
php artisan test --filter=ApiAuthenticationTest
php artisan test --filter=CalendarTest
php artisan test --filter=EventTest
php artisan test --filter=InviteTest
# Run with coverage
php artisan test --coverage
# Run critical async email test
php artisan test --filter=test_invitation_email_is_queued_not_sent_synchronouslytests/Feature/Auth/ApiAuthenticationTest.php- API authenticationtests/Feature/CalendarTest.php- Calendar CRUD operationstests/Feature/EventTest.php- Event managementtests/Feature/InviteTest.php- Invitation system with queue verification
php artisan serveAPI will be available at: http://localhost:8000/api
IMPORTANT: Queue worker must be running to process invitation emails.
# Development (processes jobs immediately)
php artisan queue:work
# With retry and timeout
php artisan queue:work --tries=3 --timeout=60
# Daemon mode (production)
php artisan queue:work --daemonFor production, use Supervisor to keep queue worker running. See Laravel docs.
POST /api/register
Content-Type: application/json
{
"name": "John Doe",
"email": "[email protected]",
"password": "password123",
"password_confirmation": "password123"
}Response (201):
{
"user": {
"id": 1,
"name": "John Doe",
"email": "[email protected]"
},
"token": "1|abc123..."
}POST /api/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "password123"
}POST /api/logout
Authorization: Bearer {token}GET /api/auth/googleRedirects to Google OAuth consent screen.
GET /api/auth/google/callbackCallback URL for Google OAuth.
All require Authorization: Bearer {token} header.
GET /api/calendarsOptional Query Parameters:
sort=-created_at- Sort by created date (descending)filter[name]=Work- Filter by namefilter[is_default]=1- Filter default calendars
POST /api/calendars
Content-Type: application/json
{
"name": "Work Calendar",
"description": "My work events",
"color": "#ff0000",
"timezone": "America/New_York"
}GET /api/calendars/{id}PUT /api/calendars/{id}
Content-Type: application/json
{
"name": "Updated Name",
"color": "#00ff00"
}DELETE /api/calendars/{id}GET /api/calendars/{calendar_id}/eventsOptional Query Parameters:
sort=start_time- Sort by start timefilter[title]=Meeting- Filter by titlefilter[start_time][gte]=2025-10-01- Events starting after datefilter[end_time][lte]=2025-10-31- Events ending before date
POST /api/calendars/{calendar_id}/events
Content-Type: application/json
{
"title": "Team Meeting",
"description": "Weekly sync",
"start_time": "2025-10-25 10:00:00",
"end_time": "2025-10-25 11:00:00",
"location": "Conference Room A",
"is_all_day": false
}GET /api/events/{id}PUT /api/events/{id}
Content-Type: application/json
{
"title": "Updated Meeting Title",
"start_time": "2025-10-25 14:00:00",
"end_time": "2025-10-25 15:00:00"
}DELETE /api/events/{id}POST /api/events/{event_id}/invite
Content-Type: application/json
Authorization: Bearer {token}
{
"invitee_email": "[email protected]"
}Response (201):
{
"data": {
"id": 1,
"event_id": 1,
"user_id": null,
"invitee_email": "[email protected]",
"status": "pending"
}
}Important: Email is queued asynchronously. No immediate email is sent.
GET /api/invites
Authorization: Bearer {token}GET /api/events/{event_id}/invites
Authorization: Bearer {token}PUT /api/invites/{invite_id}/accept
Authorization: Bearer {token}PUT /api/invites/{invite_id}/reject
Authorization: Bearer {token}The API uses Laravel Policies for authorization:
- CalendarPolicy: Users can only access their own calendars
- EventPolicy: Users can only manage events in their calendars
- InvitePolicy: Users can accept/reject their own invites
Unauthorized requests return 403 Forbidden.
- User creates invite via
POST /api/events/{event}/invite - Invite record created in database
SendEventInvitationjob queued (not executed immediately)- API responds immediately (non-blocking)
- Queue worker processes job asynchronously
- Email sent via configured mail driver
// From tests/Feature/InviteTest.php
public function test_invitation_email_is_queued_not_sent_synchronously(): void
{
Mail::fake();
Queue::fake();
// Create invite
$response = $this->postJson("/api/events/{$event->id}/invite", [
'invitee_email' => '[email protected]',
]);
// Assert NO synchronous emails
Mail::assertNothingOutbox();
// Assert job WAS queued
Queue::assertPushed(SendEventInvitation::class);
}Development (synchronous for testing):
QUEUE_CONNECTION=syncProduction (asynchronous):
QUEUE_CONNECTION=database
# or
QUEUE_CONNECTION=redisRun worker:
php artisan queue:workMinimal Livewire dashboard for admins to manage users, calendars, events, and view invite statuses.
Access: /admin
Features:
- User management (CRUD)
- Calendar overview
- Event management
- Invite status tracking
Setup:
# Create admin user
php artisan tinker
>>> $user = User::find(1);
>>> $user->assignRole('admin');- Register/Login: Call
/api/registeror/api/login - Store Token: Save
tokenfrom response securely - API Requests: Include
Authorization: Bearer {token}header - Google OAuth: Use webview for
/api/auth/googleflow
- Local Notifications: Mobile app handles all reminders
- Offline Support: Cache calendars/events, sync when online
- Calendar Sync: Poll
/api/calendarsand/api/eventsendpoints - Invite Notifications: Poll
/api/invitesor implement webhooks - Push Notifications: Implement using Firebase/APNs (not in backend)
- Use appropriate HTTP methods (GET, POST, PUT, DELETE)
- Check response status codes (200, 201, 401, 403, 422, 500)
- Handle validation errors (422 returns field-specific errors)
- Implement token refresh logic
- Handle network errors gracefully
app/
βββ Http/
β βββ Controllers/Api/ # API controllers
β βββ Requests/ # Form validation
β βββ Resources/ # API responses
βββ Jobs/ # Queue jobs
βββ Mail/ # Mailables
βββ Models/ # Eloquent models
βββ Policies/ # Authorization policies
database/
βββ factories/ # Model factories
βββ migrations/ # Database schema
βββ seeders/ # Seed data
tests/Feature/ # Feature tests
βββ Auth/
βββ CalendarTest.php
βββ EventTest.php
βββ InviteTest.php
resources/views/emails/ # Email templates
- Ensure queue worker is running:
php artisan queue:work - Check
QUEUE_CONNECTIONin.env - View failed jobs:
php artisan queue:failed - Retry failed:
php artisan queue:retry all
- Run
php artisan config:clear - Ensure test database is configured
- Check
.env.testingfile
- Verify credentials in
.env - Check redirect URI matches Google Console
- Ensure app is in testing mode (not production)
- Verify policies are registered in
AuthServiceProvider - Check user owns the resource being accessed
- Default calendar automatically created on user registration
- Invites support both registered users (user_id) and unregistered (email only)
- Cascading deletes: Deleting calendar deletes events and invites
- All timestamps in UTC, convert in mobile app for local timezone
- Color validation: Must be 7-character hex code (#RRGGBB)
- Set
APP_ENV=productionin.env - Set
APP_DEBUG=false - Use database/Redis queue driver
- Configure Supervisor for queue worker
- Setup SSL certificate
- Configure CORS for mobile app domains
- Enable rate limiting
- Setup monitoring (Sentry, Bugsnag, etc.)
- Configure backup strategy
- Setup CI/CD pipeline
[program:calendros-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasec=3600
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/path/to/storage/logs/worker.log
stopwaitsecs=3600This project is open-sourced software licensed under the MIT license.
- Fork the repository
- Create feature branch (
git checkout -b feature/amazing-feature) - Write tests for new features
- Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open Pull Request
For issues, questions, or contributions, please open an issue on GitHub.
Built with β€οΈ using Laravel, following TDD principles