18 KiB
Architecture
System Overview
SIDES (Sistem Informasi Data dan Early Warning System) is a Laravel 12 web application that collects, stores, and displays real-time environmental monitoring data from rainfall, water level, and siren stations. It provides a dashboard with a map view of station status, historical data with export capabilities (Excel, PDF), and push notifications via Firebase Cloud Messaging (FCM). The system runs entirely in Docker containers using nginx as a reverse proxy, PHP-FPM for application code, and PostgreSQL 16 for persistence. External IoT stations submit readings through a public REST API; authenticated admin users manage stations and user accounts through a Blade-based web UI.
Container Architecture
+------------------+
| User Browser |
+--------+---------+
|
| :80
v
+--------+---------+
| web (nginx) |
| nginx:stable- |
| alpine |
+--------+---------+
|
fastcgi :9000
|
+--------+---------+
./src -----> | app (php-fpm) |
(bind mount) | php:8.2-fpm |
+--------+---------+
|
pgsql :5432
|
+--------+---------+ +------------------+
| postgres | | pgadmin |
| postgres:16 | | dpage/pgadmin4 |
+------------------+ +------------------+
volume: pgdata volume: pgadmin_data
flowchart TD
UB["User Browser"]:::client -->|:80| N["web (nginx)\nnginx:stable-alpine"]:::server
N -->|fastcgi :9000| A["app (php-fpm)\nphp:8.2-fpm\n(bind mount ./src)"]:::app
A -->|pgsql :5432| DB["postgres\npostgres:16\nvolume: pgdata"]:::db
PG["pgAdmin\ndpage/pgadmin4\nvolume: pgadmin_data"]:::tool --> DB
classDef client fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
classDef app fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
classDef tool fill:#FFC107,stroke:#333,stroke-width:2px,color:#000;
Container Summary
| Container | Image | Port | Purpose |
|---|---|---|---|
| tckdev-app | php:8.2-fpm (custom) | 9000 | Laravel application (PHP-FPM) |
| tckdev-db | postgres:16 | 5432 | PostgreSQL database |
| tckdev-web | nginx:stable-alpine | 80 | Reverse proxy, serves static assets |
| tckdev-pgAdmin | dpage/pgadmin4 | 5050 | Database administration UI |
Networking and Volumes
- All containers share the
tckdev_netbridge network. src/is bind-mounted into bothappandwebat/var/www/html, so code changes are reflected without rebuilding.pgdataandpgadmin_dataare Docker named volumes -- not bind mounts.- The nginx config is bind-mounted from
docker/nginx/default.conf. docker/backupis bind-mounted into pgadmin at/backups.
Entrypoint Behaviour
The app container runs docker/entrypoint.sh before starting PHP-FPM. This script:
- Copies
.env.exampleto.envif no.envexists. - Runs
composer installifvendor/is missing. - Runs
npm installifnode_modules/is missing. - Runs
npm run build(Vite) ifpublic/build/is missing. - Runs
php artisan migrate --forcewhenRUN_MIGRATIONS=true. - Runs
php artisan db:seed --forcewhenRUN_SEEDER=true. - Caches config, routes, and views for production performance.
Request Flow
Web UI Request (Blade pages)
Browser --:80--> nginx
|
+-- static asset? --> /var/www/html/public/{file}
|
+-- *.php? ---------> fastcgi://app:9000
|
Laravel Router
|
+-- auth middleware --> Breeze session auth
+-- admin middleware --> access_level === 1 check
|
Controller
|
DB::table() / DB::select()
|
v
PostgreSQL
|
Blade view / JSON response
flowchart TD
B["Browser"]:::client -->|:80| N["nginx"]:::server
N -->|static asset?| SA["/var/www/html/public/{file}"]:::static
N -->|*.php?| P["fastcgi://app:9000"]:::php
P --> R["Laravel Router"]:::router
R --> AM["auth middleware\n(Breeze session auth)"]:::middleware
R --> ADM["admin middleware\n(access_level === 1 check)"]:::middleware
R --> C["Controller"]:::controller
C --> Q["DB::table() / DB::select()"]:::query --> DB["PostgreSQL"]:::db
DB --> V["Blade view / JSON response"]:::response
classDef client fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
classDef static fill:#9E9E9E,stroke:#333,stroke-width:2px,color:#fff;
classDef php fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
classDef router fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff;
classDef middleware fill:#FFC107,stroke:#333,stroke-width:2px,color:#000;
classDef controller fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff;
classDef query fill:#795548,stroke:#333,stroke-width:2px,color:#fff;
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
classDef response fill:#607D8B,stroke:#333,stroke-width:2px,color:#fff;
- Browser hits port 80 on the
webcontainer. - nginx serves static files from
/var/www/html/publicdirectly. - PHP requests are forwarded via FastCGI to
app:9000. - Laravel routes the request through middleware (
auth,admin, or none). - Controllers query PostgreSQL using
DB::table()orDB::select()with parameterized bindings. - Responses are rendered Blade views (HTML) or JSON for AJAX endpoints.
API Request (IoT station data)
IoT Device --:80/api/...--> nginx ---> app:9000
|
Laravel API Router (no auth middleware on data endpoints)
|
Controller (Api\*)
|
DB::table() / DB::select()
|
v
PostgreSQL
flowchart TD
D["IoT Device"]:::device -->|:80/api/...| N["nginx"]:::server
N --> P["app:9000 (php-fpm)"]:::php
P --> R["Laravel API Router\n(no auth middleware on data endpoints)"]:::router
R --> C["Controller (Api\\*)"]:::controller
C --> Q["DB::table() / DB::select()"]:::query --> DB["PostgreSQL"]:::db
classDef device fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
classDef php fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
classDef router fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff;
classDef controller fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff;
classDef query fill:#795548,stroke:#333,stroke-width:2px,color:#fff;
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
API routes under /api/ have no middleware. The POST /api/login endpoint validates credentials against the users table using DB::select() with bound parameters and Hash::check(). There are no API tokens -- authentication is a single request/response returning user data on success.
Data Model
The application uses six database tables. There are no foreign key constraints; relationships are enforced at the application level. User is the only Eloquent model. All other data access uses the DB query builder with parameterized queries.
+----------------+ +----------------+ +----------------+
| station | | rainfall | | waterlevel |
|----------------| |----------------| |----------------|
| stationid (PK) |<--+---| id (PK) | +----| id (PK) |
| name | | | stationid | | | stationid |
| district | | | timestamp | | | datetime |
| lng | | | anncum | | | waterlevel |
| lat | | | daily | | | alert |
| mainriverbasin | | | hourly | | | warning |
| subriverbasin | | | currentrf | | | danger |
| rainfall (flag)| | | battery | | +----------------+
| waterlevel (f) | | +----------------+ |
| siren (flag) | | |
| cctv_link | | +----------------+ | +----------------+
+----------------+ | | siren | | | notification |
+---| id (PK) | +---| id (PK) |
| stationid | | stationid |
| stationtype | | timestamp |
| active_time | | stationtype |
| level | | level |
+----------------+ | active_time |
+----------------+
+----------------+
| users |
|----------------|
| id (PK) |
| name |
| email |
| access_level |
| password |
| login_attempts |
| is_blocked |
+----------------+
erDiagram
station {
int stationid
string name
string district
float lng
float lat
string mainriverbasin
string subriverbasin
boolean rainfall_flag
boolean waterlevel_flag
boolean siren_flag
string cctv_link
}
rainfall {
int id
int stationid
datetime timestamp
float anncum
float daily
float hourly
float currentrf
float battery
}
waterlevel {
int id
int stationid
datetime datetime
float waterlevel
string alert
string warning
string danger
}
siren {
int id
int stationid
string stationtype
datetime active_time
string level
}
notification {
int id
int stationid
datetime timestamp
string stationtype
string level
datetime active_time
}
users {
int id
string name
string email
string access_level
string password
int login_attempts
boolean is_blocked
}
station ||--o{ rainfall : "has"
station ||--o{ waterlevel : "has"
station ||--o{ siren : "has"
station ||--o{ notification : "has"
Table Descriptions
| Table | Purpose |
|---|---|
station |
Master list of monitoring stations with location and capability flags |
rainfall |
Timestamped rainfall readings per station |
waterlevel |
Timestamped water level readings with alert/warning/danger thresholds |
siren |
Siren activation events with level (N/D/W/A) |
notification |
Notification log entries linking stations to alert levels |
users |
Application users; access_level=1 for admin |
Application Layer
Controllers
Web controllers (render Blade views):
| Controller | Responsibility |
|---|---|
MapController |
Dashboard/map view; joins station with latest readings |
RainfallController |
Rainfall display, graphs, historical data, Excel export |
WaterLevelController |
Water level display, graphs, historical data, Excel export |
SirenController |
Siren status display, history, PDF export |
NotificationController |
Rainfall/water level/siren notification views and PDF exports |
AdminController |
Station and user CRUD (admin-only) |
cctvController |
CCTV feed display |
LocaleController |
Language switching |
ProfileController |
User profile management (Breeze) |
| Auth controllers | Login, register, password reset (Breeze scaffolding) |
API controllers (return JSON):
| Controller | Responsibility |
|---|---|
Api\StationController |
Current data, rainfall, water level, notifications, siren status for IoT/external consumption |
Api\AuthController |
Token-less login; validates username/password, returns user JSON |
Api\AlertController |
Routes FCM push notifications to the correct topic based on stationtype and level |
Middleware
| Middleware | Behavior |
|---|---|
auth |
Redirects unauthenticated users to login (Breeze) |
admin |
Checks access_level === 1; redirects non-admins to dashboard |
LocalizationMiddleware |
Sets application locale from session |
Services
| Service | Responsibility |
|---|---|
FcmService |
Obtains Google OAuth2 access token from Firebase service account credentials, sends FCM v1 API messages to topics |
FCM Topic Routing
AlertController::send() determines the FCM topic from the request's stationtype and level:
| stationtype | Level | FCM Topic |
|---|---|---|
| 1 | Warning | FCM_TOPIC_RAINFALL_WARNING |
| 1 | Danger | FCM_TOPIC_RAINFALL_DANGER |
| 2 | Warning | FCM_TOPIC_WATERLEVEL_ALERT |
| 2 | Danger | FCM_TOPIC_WATERLEVEL_DANGER |
| 3 | (any) | FCM_TOPIC_RAINFALL_WARNING |
Authentication
Two separate authentication mechanisms coexist:
- Web UI: Laravel Breeze provides session-based authentication (login, register, password reset, email verification). The
authmiddleware guards protected routes. - API:
POST /api/loginacceptsusernameandpassword, validates against theuserstable, and returns user data as JSON. There is no token or session persisted for API consumers -- each API data endpoint is stateless and unauthenticated.
Frontend Stack
- Blade templates with Tailwind CSS (via
tailwind.config.js). - Vite handles asset bundling (
vite.config.js,postcss.config.js). - Map rendering uses Leaflet with station markers.
- Client-side JavaScript in
public/js/handles graph rendering and AJAX calls.
Database Configuration
- Connection:
pgsql(PostgreSQL 16) -- configured in.envasDB_CONNECTION=pgsql. - Host:
postgres(Docker service name). - Session driver:
database(Laravel's database session storage). - Cache store:
database. - Queue connection:
database.
All session, cache, and queue data lives in PostgreSQL tables created by Laravel's default migrations (cache, jobs, sessions tables).
Directory Structure
tckdev/
docker/ Docker configuration
entrypoint.sh App container startup script
nginx/
default.conf nginx reverse proxy config
src/ Laravel application (bind-mounted into containers)
app/
Http/
Controllers/ Web controllers
Controllers/Api/ API controllers (StationController, AuthController, AlertController)
Middleware/ AdminMiddleware, LocalizationMiddleware
Requests/ Form request validation
Exports/ Maatwebsite Excel export classes
Models/ User (only Eloquent model)
Notifications/ ResetPasswordNotification
Services/ FcmService
Providers/ AppServiceProvider
View/Components/ AppLayout, GuestLayout (Blade components)
bootstrap/
config/ Laravel config files
database/
migrations/ Schema migrations (13 files)
seeders/ Database seeders
factories/ Model factories
public/ Document root (served by nginx)
resources/
views/ Blade templates
routes/
web.php Web routes (authenticated + admin + public)
api.php API routes (stateless, no auth middleware)
auth.php Breeze auth routes
console.php Artisan commands
storage/ App storage (logs, firebase credentials, uploads)
tests/ PHPUnit tests
docker-compose.yml Service definitions (Compose v2, no version key)
Dockerfile Custom php:8.2-fpm image