Deploiement Scell.io
Version: 1.0 Date: 2026-03-03
Table des matieres
- Infrastructure
- Prerequis
- Deploiement Backend (api.scell.io)
- Deploiement Frontend (scell.io)
- Variables d'Environnement
- Monitoring
- Troubleshooting
1. Infrastructure
Architecture de deploiement
graph TB
subgraph "Internet"
U["Utilisateurs"]
AI["Agents IA (MCP)"]
SPDP["SuperPDP"]
OA["OpenAPI.com"]
end
subgraph "Ploi.io (Scaleway)"
subgraph "scell.io"
FE["Frontend React<br/>Static (Vite build)<br/>dist/"]
end
subgraph "api.scell.io"
BE["Laravel 12<br/>PHP 8.2+<br/>backend/public/"]
HZ["Horizon<br/>(Queue Worker)"]
end
subgraph "Services Manages (Scaleway)"
PG["PostgreSQL 17<br/>(base principale)"]
PGS["PostgreSQL 17<br/>(base sandbox)"]
RD["Redis 7.4<br/>(cache + sessions + queue)"]
S3["Scaleway S3<br/>(stockage factures/docs)"]
end
end
U --> FE
U --> BE
AI -->|"MCP JSON-RPC"| BE
SPDP -->|"Webhooks"| BE
OA -->|"Webhooks"| BE
BE --> PG
BE --> PGS
BE --> RD
BE --> S3
BE -->|"API"| SPDP
BE -->|"API"| OA
HZ --> PG
HZ --> RDComposants
| Composant | Domaine | Type | Stack |
|---|---|---|---|
| Frontend | scell.io | Site statique | React 19 + Vite (dist/) |
| Backend | api.scell.io | Application PHP | Laravel 12 + PHP 8.2 |
| Base de donnees | - | Service manage | PostgreSQL 17 (Scaleway) |
| Cache/Queue | - | Service manage | Redis 7.4 (Scaleway) |
| Stockage | - | Service manage | Scaleway S3 (fr-par) |
Hebergement Ploi.io
Ploi.io gere :
- Le provisionning des serveurs Scaleway
- Les certificats SSL Let's Encrypt (renouvellement automatique)
- Le deploiement automatise via webhook Git
- La supervision des processus (PHP-FPM, Horizon, daemon WebSocket Reverb)
Serveur : Scaleway DEV1-M (3 vCPU, 4GB RAM) SSH : ssh ploi@51.15.234.246Chemins :
| Site | Chemin sur le serveur |
|---|---|
| Backend | /home/ploi/api.scell.io/backend |
| Frontend | /home/ploi/scell.io/frontend |
2. Prerequis
Backend
| Composant | Version minimale | Verification |
|---|---|---|
| PHP | 8.2+ | php -v |
| Composer | 2.x | composer --version |
| PostgreSQL | 17 | psql --version |
| Redis | 7.4 | redis-cli --version |
| wkhtmltopdf | 0.12+ | wkhtmltopdf --version |
| LibreOffice (headless) | 24.2+ | soffice --headless --version |
Extensions PHP requises :
bcmath, ctype, curl, dom, fileinfo, gd, intl, json, mbstring,
openssl, pdo, pdo_pgsql, redis, tokenizer, xml, zipLibreOffice — conversion Word -> PDF (wizard de signature)
Le wizard de signature convertit les documents Word (.docx, .doc) en PDF fidele cote serveur via LibreOffice headless. Sans cette dependance, l'endpoint POST /api/v1/signatures/convert-document renvoie 422.
# Debian / Ubuntu (24.04+) — paquets minimaux (headless, ~350 Mo)
sudo apt-get install --no-install-recommends \
libreoffice-writer libreoffice-core \
fonts-liberation fonts-dejavu \
fonts-crosextra-carlito fonts-crosextra-caladeaLes polices
fonts-crosextra-carlito(≈ Calibri) etfonts-crosextra-caladea(≈ Cambria) sont metriques-compatibles : elles garantissent une pagination fidele aux.docxWord sans les polices Microsoft (non redistribuables). Ne pas les omettre. Variables d'env optionnelles :LIBREOFFICE_BIN,LIBREOFFICE_TIMEOUT.
Frontend
| Composant | Version minimale | Verification |
|---|---|---|
| Node.js | 20+ | node --version |
| npm | 10+ | npm --version |
Certificats SSL
Les certificats Let's Encrypt sont geres automatiquement par Ploi.io :
/etc/letsencrypt/live/api.scell.io/fullchain.pem
/etc/letsencrypt/live/api.scell.io/privkey.pem3. Deploiement Backend (api.scell.io)
Script de deploiement
#!/bin/bash
# deploy-backend.sh
set -e
cd /home/ploi/api.scell.io/backend
# 1. Mettre le site en maintenance
php artisan down --render="errors::503" --retry=60
# 2. Recuperer les derniers changements
git pull origin main
# 3. Installer les dependances (production)
composer install --no-dev --optimize-autoloader --no-interaction
# 4. Executer les migrations
php artisan migrate --force
# 5. Mettre en cache la configuration
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# 6. Nettoyer les caches obsoletes (AVANT les signaux de restart : cache:clear
# effacerait le signal `laravel:reverb:restart`)
php artisan cache:clear
# 7. Redemarrer les workers de queue et le serveur WebSocket
php artisan horizon:terminate
php artisan reverb:restart
# Horizon et Reverb sont relances automatiquement par leur superviseur
# (smoke check Reverb non bloquant : voir "Serveur WebSocket Reverb")
# 8. Remettre le site en ligne
php artisan up
echo "Deploiement backend termine avec succes."Commandes post-deploiement
# Verifier que Horizon tourne
php artisan horizon:status
# Verifier que Reverb (WebSocket) repond — voir "Serveur WebSocket Reverb"
curl -fsS http://127.0.0.1:6001/up
# Verifier la sante de l'API
curl -s https://api.scell.io/health | jq .
# Verifier les migrations
php artisan migrate:statusConfiguration Nginx (reference)
Le fichier nginx.conf.example a la racine du projet contient la configuration de reference :
server {
listen 443 ssl http2;
server_name api.scell.io;
root /home/ploi/api.scell.io/backend/public;
index index.php;
ssl_certificate /etc/letsencrypt/live/api.scell.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.scell.io/privkey.pem;
# CORS headers
add_header Access-Control-Allow-Origin "https://scell.io" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-API-Key, X-Tenant-Key, X-API-Key, X-Scell-API-Key, mcp-session-id" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
# SSE pour MCP streaming
location /mcp/stream {
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
try_files $uri /index.php?$query_string;
}
}Superviseur Horizon
Ploi.io configure automatiquement le superviseur pour Horizon :
[program:horizon]
process_name=%(program_name)s
command=php /home/ploi/api.scell.io/backend/artisan horizon
autostart=true
autorestart=true
user=ploi
redirect_stderr=true
stdout_logfile=/home/ploi/api.scell.io/backend/storage/logs/horizon.log
stopwaitsecs=3600Serveur WebSocket Reverb
BROADCAST_CONNECTION=reverb suppose un serveur Laravel Reverb en fonctionnement. Sans lui, les events temps reel (statuts factures, signatures, ledger) ne sont jamais delivres au dashboard, sans aucune erreur cote API. Le daemon et le proxy nginx sont provisionnes cote serveur (panel Ploi), hors du repo ; le repo porte la configuration (config/reverb.php), le restart au deploy et le smoke check.
Chaine complete :
Dashboard (app.scell.io / app.jefacturebien.fr)
└─ wss://api.scell.io/app/{REVERB_APP_KEY} VITE_REVERB_HOST=api.scell.io, port 443, https
└─ nginx `location /app/` → http://127.0.0.1:6001 (Upgrade / Connection)
└─ daemon supervisor : php artisan reverb:start --host=127.0.0.1 --port=6001
Backend (API + Horizon) → POST http://127.0.0.1:6001/apps/{REVERB_APP_ID}/events (REVERB_HOST:REVERB_PORT)1. Daemon supervisor (Ploi → Server → Daemons)
[program:reverb]
process_name=%(program_name)s
command=php /home/ploi/api.scell.io/backend/artisan reverb:start --host=127.0.0.1 --port=6001
directory=/home/ploi/api.scell.io/backend
autostart=true
autorestart=true
user=ploi
redirect_stderr=true
stdout_logfile=/home/ploi/api.scell.io/backend/storage/logs/reverb.log
stopwaitsecs=30
stopasgroup=true
killasgroup=true- Ploi nomme le programme
worker-<id>(et nonreverb) : l'identifier avecgrep -l 'reverb:start' /etc/supervisor/conf.d/*.conf. --host/--portdoivent rester identiques aREVERB_SERVER_HOST/REVERB_SERVER_PORT(sans option,reverb:startlit ces valeurs). Le port serveur doit etre celui deREVERB_PORT: c'est la que le backend publie les events.- Ne jamais binder
0.0.0.0: le serveur n'est joignable que via nginx (TLS + filtrage). - Beaucoup de connexions simultanees : relever la limite de descripteurs du daemon (
minfdssupervisor /ulimit -n).
2. Nginx (api.scell.io, et api.jefacturebien.fr si VITE_REVERB_HOST y pointe)
# WebSocket Reverb (protocole Pusher). Prefixe `/app/` AVEC slash final : ne capture
# pas `/apps/*` (API HTTP de publication, reservee au backend en loopback).
location /app/ {
proxy_pass http://127.0.0.1:6001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# > REVERB_APP_PING_INTERVAL (60 s) sinon nginx coupe les sockets inactives
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}3. Origines autorisees
REVERB_ALLOWED_ORIGINS (defaut app.scell.io,app.jefacturebien.fr) liste les hotes autorises a ouvrir une socket : Reverb compare l'hote de l'en-tete Origin a chaque entree (motifs *.domaine acceptes). Les URLs completes sont normalisees en hote et le joker global * est toujours ignore. Une origine refusee recoit, apres l'upgrade, un pusher:error code 4009. Variables : voir section 5 "WebSockets (Reverb)".
4. Deploiement et smoke checks
scripts/deploy-api.sh execute php artisan reverb:restart apres horizon:terminate (signal en cache lu toutes les 5 s par le serveur, relance par le superviseur), puis sonde http://127.0.0.1:6001/up. Ce smoke est non bloquant : il affiche WARN reverb down si aucun serveur ne repond (daemon non provisionne) sans faire echouer le deploy.
Apres toute modification des variables REVERB_* : php artisan config:cache && php artisan reverb:restart.
Verifications manuelles :
# Daemon en ecoute + sante
ss -tln | grep ':6001 '
curl -fsS http://127.0.0.1:6001/up # {"health":"OK"}
sudo supervisorctl status | grep -E 'reverb|worker-'
# Proxy nginx + origine : attendu "HTTP/1.1 101 Switching Protocols"
# (404 = `location /app/` absente, la requete est tombee sur Laravel)
curl -si --http1.1 --max-time 5 \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
-H "Origin: https://app.scell.io" \
"https://api.scell.io/app/${REVERB_APP_KEY}?protocol=7" | head -n 15. Dashboard : variables VITE_REVERB_* (build Vite)
Le client Echo du dashboard lit sa configuration au build (import.meta.env, valeurs inlinees dans le bundle). Un bundle builde sans ces variables n'a pas de temps reel.
| Variable | Valeur prod | Source / role |
|---|---|---|
VITE_REVERB_APP_KEY | valeur de REVERB_APP_KEY du backend | Cle publique Reverb (jamais REVERB_APP_SECRET). Obligatoire pour activer le temps reel |
VITE_REVERB_HOST | api.scell.io | Hote public servant location /app/ (nginx). Obligatoire pour activer le temps reel |
VITE_REVERB_PORT | 443 | Port public TLS. Defaut 443 si absent ou invalide |
VITE_REVERB_SCHEME | https | https active forceTLS ; http reserve au dev local. Defaut https |
Fichier persistant hors git : /home/ploi/scell.io/frontend/.env.production (gitignore, survit aux git pull / git stash ; modele versionne : frontend/.env.production.example). scripts/deploy-dashboard.sh et scripts/deploy-website.sh (les deux buildent le dashboard) chargent ces 4 cles via scripts/lib/dashboard-realtime-env.sh juste avant vite build. Une variable deja exportee dans l'environnement du deploy prime sur le fichier. Seules les lignes VITE_REVERB_*=valeur sont lues : le fichier n'est jamais sourcee.
- Cles obligatoires absentes : le deploy affiche
WARN temps reel desactive ...et continue ; le bundle est fonctionnel, sans rafraichissement live. - Cles presentes : smoke post-build bloquant, la cle doit etre inlinee dans
dist/assets/*.js(sinon❌ smoke temps reelet le deploy echoue). LeswsHost:"..."litteraux eventuels sont affiches a titre informatif (le minifier peut garder l'hote dans une variable).
Hote WebSocket unique pour les deux canaux : le build est partage entre app.scell.io et app.jefacturebien.fr, qui ouvrent tous deux la socket sur wss://api.scell.io (l'hote app.jefacturebien.fr doit donc figurer dans REVERB_ALLOWED_ORIGINS). Pointer VITE_REVERB_HOST vers api.jefacturebien.fr exigerait la location /app/ sur ce vhost et s'appliquerait aussi a app.scell.io.
Authentification des channels prives, canal-aware au runtime : POST {resolveApiBaseUrl()}/broadcasting/auth, soit https://api.scell.io/api/v1/broadcasting/auth ou https://api.jefacturebien.fr/api/v1/broadcasting/auth (cookie de session same-domain + jeton XSRF). La route est enregistree sous /api/v1 par withBroadcasting(routes/channels.php, ['prefix' => 'api/v1', 'middleware' => ['api', 'auth:sanctum']]) (bootstrap/app.php) : invite → 401 JSON, tenant etranger → 403. Elle est couverte par le motif CORS api/* (plus d'entree dediee dans cors.paths).
Comportement runtime (frontend/src/lib/echo.ts) : sans VITE_REVERB_APP_KEY ou VITE_REVERB_HOST, getEcho() retourne null et emet une seule fois console.warn("[Echo] temps reel desactive : ...") ; useTenantChannel ne souscrit alors a rien. Aucune page ne doit dependre du temps reel pour fonctionner.
Noms d'events : les events App\Events\Tenant\* publient leur broadcastAs() (nom court de la classe). Echo prefixant tout nom par le namespace App.Events sauf s'il commence par ., useTenantChannel ecoute .InvoiceCreated, .CreditNoteSent, etc. La liste typee TENANT_BROADCAST_EVENTS (frontend/src/hooks/use-tenant-channel.ts) est le miroir des 11 sous-classes de AbstractTenantBroadcastEvent : tout ajout ou renommage cote backend doit y etre reporte (les handlers d'un event absent de la liste ne compilent pas).
Verification apres deploy :
# La cle publique doit apparaitre dans le bundle servi (vide = temps reel non inline)
KEY="$(grep '^VITE_REVERB_APP_KEY=' /home/ploi/scell.io/frontend/.env.production | cut -d= -f2-)"
grep -rlF --include='*.js' "$KEY" /home/ploi/scell.io/frontend/dist/assets | head -n 1
grep -rhoE --include='*.js' 'wsHost:"[^"]*"' /home/ploi/scell.io/frontend/dist/assets | sort -uTaches planifiees (Cron)
# Ajouter au crontab de l'utilisateur ploi
* * * * * cd /home/ploi/api.scell.io/backend && php artisan schedule:run >> /dev/null 2>&1Taches planifiees automatiques :
| Commande | Frequence | Description |
|---|---|---|
fiscal:daily-closing | Quotidien 00:05 UTC | Cloture fiscale journaliere |
fiscal:monthly-closing | 1er du mois | Cloture fiscale mensuelle |
fiscal:integrity-check | Quotidien 02:00 UTC | Verification d'integrite |
fiscal:check-version-change | Quotidien | Detection changement de version |
billing:generate-monthly-invoices | 1er du mois | Factures tenants |
superpdp:sync-statuses | Toutes les 5 min | Synchronisation statuts |
horizon:snapshot | Toutes les 5 min | Metriques Horizon |
4. Deploiement Frontend (scell.io)
Script de deploiement
#!/bin/bash
# deploy-frontend.sh
set -e
cd /home/ploi/scell.io/frontend
# 1. Recuperer les derniers changements
git pull origin main
# 2. Installer les dependances
npm ci
# 3. Build de production
npm run build
echo "Deploiement frontend termine. Le dossier dist/ est pret."Le dossier dist/ genere par Vite est servi directement par Nginx comme site statique.
Builds bornes en memoire et rebuild du dashboard
Incident du 2026-10-14 : sur l'instance DEV1-S (2 Go), le build Vite lance avec --max-old-space-size=4096 a fait swapper toute la machine jusqu'au gel. API, dashboard et sites ont ete hors service pendant environ 20 min. L'instance est depuis passee en DEV1-M (4 Go). scripts/deploy-website.sh et scripts/deploy-dashboard.sh appliquent en outre deux protections :
- Plafond memoire par build (
scripts/lib/bounded-build.sh) : type-check,vite buildet les deuxnext buildtournent chacun dans un scope cgroupsystemd-run --useravecMemoryMax=2816M,MemorySwapMax=1536M,RuntimeMaxSec=1800, et un tas V8 plafonne a 2048 Mo. Un depassement tue le build seul : le deploy echoue, la prod reste servie, et le messagetue par le plafond memoire du cgroups'affiche. Chaque build journalise son pic ([build:<nom>] pic memoire cgroup). Le deploy refuse de demarrer siMemAvailable< 1536 Mo. Le linger systemd deploiest active automatiquement : sans lui, la fin de la session SSH ou du hook arreterait le build en cours. Les valeurs se surchargent viaSCELL_BUILD_*. - Dashboard reconstruit seulement si ses entrees changent (
scripts/lib/dashboard-publish.sh) : l'empreinte couvre l'arbre gitfrontend/, les scripts de publication, la version de Node, les variablesVITE_*(horsVITE_RELEASE) et les fichiers dotenv defrontend/. Si l'empreinte et leindex.htmlservi correspondent au marqueur/home/ploi/.scell-dashboard-published, le build est saute (Dashboard inchange). Un bundle inchange garde la release du commit qui l'a produit, celle de ses source maps.SCELL_FORCE_DASHBOARD_BUILD=1force la reconstruction.
Tests : bash scripts/tests/dashboard-publish.test.sh (job CI deploy-scripts-test).
Configuration Nginx
server {
listen 443 ssl http2;
server_name scell.io www.scell.io;
root /home/ploi/scell.io/frontend/dist;
index index.html;
ssl_certificate /etc/letsencrypt/live/scell.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/scell.io/privkey.pem;
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Cache des assets statiques
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}Variables d'environnement frontend
Le fichier /home/ploi/scell.io/frontend/.env.production (gitignore, persistant hors git ; modele frontend/.env.production.example) :
VITE_API_URL=https://api.scell.io/api/v1
VITE_APP_NAME=Scell.io
VITE_REVERB_APP_KEY=<valeur de REVERB_APP_KEY du backend>
VITE_REVERB_HOST=api.scell.io
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=httpsCes variables sont integrees au build Vite (prefixe VITE_). Les VITE_REVERB_* sont optionnelles (absentes = dashboard sans temps reel) et verifiees au deploy : voir "Serveur WebSocket Reverb" §5.
5. Variables d'Environnement
Backend (backend/.env)
Application
| Variable | Description | Exemple |
|---|---|---|
APP_NAME | Nom de l'application | Scell.io |
APP_ENV | Environnement | production |
APP_KEY | Cle de chiffrement (generer avec php artisan key:generate) | base64:... |
APP_DEBUG | Mode debug (toujours false en production) | false |
APP_TIMEZONE | Fuseau horaire | Europe/Paris |
APP_URL | URL de base de l'API | https://api.scell.io |
APP_LOCALE | Langue par defaut | fr |
Base de donnees
| Variable | Description | Exemple |
|---|---|---|
DB_CONNECTION | Driver de BDD | pgsql |
DB_HOST | Hote PostgreSQL | rdb-xxx.scw.cloud |
DB_PORT | Port | 5432 |
DB_DATABASE | Nom de la base | scell_production |
DB_USERNAME | Utilisateur | scell |
DB_PASSWORD | Mot de passe | *** |
DB_HOST_SANDBOX | Hote BDD sandbox (optionnel) | 127.0.0.1 |
DB_DATABASE_SANDBOX | Base sandbox | scell_sandbox |
Cache et Queue
| Variable | Description | Exemple |
|---|---|---|
CACHE_STORE | Driver de cache | redis |
CACHE_PREFIX | Prefixe des cles cache | scell_ |
QUEUE_CONNECTION | Driver de queue | redis |
SESSION_DRIVER | Driver de session | redis |
REDIS_HOST | Hote Redis | redis-xxx.scw.cloud |
REDIS_PORT | Port Redis | 6379 |
REDIS_PASSWORD | Mot de passe Redis | *** |
HORIZON_PREFIX | Prefixe Horizon | scell_horizon: |
WebSockets (Reverb)
| Variable | Description | Exemple |
|---|---|---|
BROADCAST_CONNECTION | Broadcaster Laravel (exige un daemon Reverb actif) | reverb |
REVERB_APP_ID | Identifiant de l'application Reverb | scell |
REVERB_APP_KEY | Cle publique (aussi VITE_REVERB_APP_KEY du dashboard) | *** |
REVERB_APP_SECRET | Secret de signature des events | *** |
REVERB_HOST | Hote ou le backend publie les events | 127.0.0.1 |
REVERB_PORT | Port ou le backend publie les events | 6001 |
REVERB_SCHEME | Schema backend → serveur (loopback) | http |
REVERB_SERVER_HOST | Adresse d'ecoute du daemon (loopback, derriere nginx) | 127.0.0.1 |
REVERB_SERVER_PORT | Port d'ecoute du daemon (defaut : REVERB_PORT) | 6001 |
REVERB_ALLOWED_ORIGINS | Hotes autorises (en-tete Origin), separes par des virgules ; * ignore | app.scell.io,app.jefacturebien.fr |
Cote dashboard (build Vite) : VITE_REVERB_APP_KEY (= REVERB_APP_KEY), VITE_REVERB_HOST=api.scell.io, VITE_REVERB_PORT=443, VITE_REVERB_SCHEME=https, lues depuis /home/ploi/scell.io/frontend/.env.production par les scripts de deploy (details, smoke et comportement sans config : "Serveur WebSocket Reverb" §5).
Mail
| Variable | Description | Exemple |
|---|---|---|
MAIL_MAILER | Driver mail | smtp |
MAIL_HOST | Serveur SMTP | smtp.tem.scw.cloud |
MAIL_PORT | Port SMTP | 587 |
MAIL_USERNAME | Utilisateur SMTP | *** |
MAIL_PASSWORD | Mot de passe SMTP | *** |
MAIL_ENCRYPTION | Chiffrement | tls |
MAIL_FROM_ADDRESS | Adresse d'envoi | noreply@scell.io |
Services externes
| Variable | Description | Exemple |
|---|---|---|
GOOGLE_CLIENT_ID | OAuth Google | xxx.apps.googleusercontent.com |
GOOGLE_CLIENT_SECRET | Secret Google OAuth | *** |
OPENAPI_BASE_URL | URL OpenAPI.com | https://esignature.openapi.com |
OPENAPI_SANDBOX_URL | URL sandbox OpenAPI | https://test.esignature.openapi.com |
OPENAPI_BEARER_TOKEN | Token OpenAPI.com | *** |
SUPERPDP_API_URL | URL SuperPDP (host unique, auth OAuth 2.1 via SUPERPDP_OAUTH2_*) | https://api.superpdp.tech |
SUPERPDP_WEBHOOK_SECRET | Secret webhooks SuperPDP | *** |
SUPERPDP_SANDBOX | Mode sandbox SuperPDP | false |
BULKGATE_APPLICATION_ID | ID app BulkGate (SMS) | *** |
BULKGATE_APPLICATION_TOKEN | Token BulkGate | *** |
INSEE (verification SIRET)
| Variable | Description | Exemple |
|---|---|---|
INSEE_API_URL | URL API Sirene | https://api.insee.fr/api-sirene/3.11 |
INSEE_API_KEY | Cle API INSEE (header X-INSEE-Api-Key-Integration) | *** |
Stockage S3
| Variable | Description | Exemple |
|---|---|---|
AWS_ACCESS_KEY_ID | Access key Scaleway | SCW... |
AWS_SECRET_ACCESS_KEY | Secret key | *** |
AWS_DEFAULT_REGION | Region | fr-par |
AWS_BUCKET | Bucket | scell-storage |
AWS_ENDPOINT | Endpoint S3 | https://s3.fr-par.scw.cloud |
AWS_USE_PATH_STYLE_ENDPOINT | Path style | true |
MCP
| Variable | Description | Exemple |
|---|---|---|
MCP_ENABLED | Activer le serveur MCP | true |
MCP_SESSION_TTL | Duree de session (secondes) | 3600 |
MCP_SESSION_DRIVER | Driver de session MCP | redis |
MCP_RATE_LIMIT | Requetes par minute | 60 |
Conformite fiscale
| Variable | Description | Exemple |
|---|---|---|
FISCAL_COMPLIANCE_ENABLED | Activer la conformite fiscale | true |
FISCAL_EDITOR_NAME | Nom de l'editeur | QR Communication SAS |
FISCAL_EDITOR_SIRET | SIRET de l'editeur | 12345678900001 |
FISCAL_LICENSE_NUMBER | Numero de licence/attestation ISCA | ISCA-AUTO-2026-001 |
FISCAL_ANCHOR_PROVIDER | Provider d'ancrage | rfc3161_tsa |
FISCAL_TSA_URL | URL du TSA | https://freetsa.org/tsr |
FISCAL_RETENTION_YEARS | Duree de retention (annees) | 6 |
PDF
| Variable | Description | Exemple |
|---|---|---|
SNAPPY_PDF_BINARY | Chemin wkhtmltopdf | /usr/bin/wkhtmltopdf |
SNAPPY_IMAGE_BINARY | Chemin wkhtmltoimage | /usr/bin/wkhtmltoimage |
6. Monitoring
Horizon Dashboard
Laravel Horizon fournit un dashboard web pour superviser les queues :
URL : https://api.scell.io/horizonAcces : Restreint aux administrateurs (authentification Sanctum + role admin).
Le dashboard affiche :
- Etat des workers (actifs, en pause, termines)
- Metriques de throughput (jobs/minute)
- Jobs en attente, en cours, echoues
- Temps d'execution moyen
- Historique des jobs
Logs Laravel
Les logs sont stockes dans backend/storage/logs/ :
| Fichier | Description |
|---|---|
laravel.log | Log general de l'application |
horizon.log | Log du worker Horizon |
scheduler.log | Log du planificateur |
Configuration dans config/logging.php :
'channels' => [
'stack' => ['driver' => 'stack', 'channels' => ['single']],
'single' => ['driver' => 'single', 'path' => storage_path('logs/laravel.log'), 'level' => 'info'],
],En production, le niveau de log est info (pas de debug).
Health Checks
Le endpoint /health retourne l'etat de l'API :
curl -s https://api.scell.io/health | jq .{
"status": "ok",
"timestamp": "2026-03-03T14:30:00+00:00"
}Le MCP health_check tool fournit un diagnostic plus detaille (database, cache).
Metriques recommandees
| Metrique | Source | Seuil d'alerte |
|---|---|---|
| Temps de reponse API (p95) | Nginx access log | > 500ms |
| Queue pending jobs | Horizon | > 1000 |
| Queue failed jobs | Horizon | > 0 |
| Espace disque | Serveur | < 20% libre |
| Memoire PHP | PHP-FPM | > 80% utilise |
| Connexions PostgreSQL | PG | > 80% du max |
| Redis memoire | Redis | > 80% du max |
7. Troubleshooting
Erreurs communes
"Connection refused" sur l'API
Symptome : L'API retourne une erreur 502 Bad Gateway.
Causes possibles :
- PHP-FPM n'est pas demarre
- Le socket Unix n'existe pas
Solution :
# Verifier le statut de PHP-FPM
sudo systemctl status php8.2-fpm
# Redemarrer si necessaire
sudo systemctl restart php8.2-fpm
# Verifier le socket
ls -la /var/run/php/php8.2-fpm.sockLes jobs de queue ne s'executent pas
Symptome : Les factures restent au statut "pending", les webhooks ne sont pas envoyes.
Solution :
# Verifier Horizon
cd /home/ploi/api.scell.io/backend
php artisan horizon:status
# Si termine, redemarrer
php artisan horizon:terminate
# Le superviseur le relancera automatiquement
# Verifier les jobs echoues
php artisan queue:failed
# Relancer les jobs echoues
php artisan queue:retry allLe dashboard ne recoit aucun event temps reel (Reverb)
Symptome : statuts de factures/signatures non rafraichis en direct, console navigateur en echec de connexion wss://api.scell.io/app/..., ou deploy affichant WARN reverb down.
Cause cote dashboard (a verifier en premier si la console affiche [Echo] temps reel desactive) : le bundle a ete builde sans VITE_REVERB_APP_KEY / VITE_REVERB_HOST (deploy affichant WARN temps reel desactive). Renseigner /home/ploi/scell.io/frontend/.env.production puis redeployer le dashboard (voir "Serveur WebSocket Reverb" §5). Un POST /api/v1/broadcasting/auth en echec CORS ou 404 indique une URL d'auth ou une config cors.paths non a jour.
Solution :
# 1. Le daemon ecoute-t-il ? (vide = daemon absent ou arrete)
ss -tln | grep ':6001 '
curl -fsS http://127.0.0.1:6001/up
grep -l 'reverb:start' /etc/supervisor/conf.d/*.conf # programme worker-<id>
sudo supervisorctl status
# 2. Relancer (apres config:cache si des variables REVERB_* ont change)
php artisan config:cache && php artisan reverb:restart
tail -50 /home/ploi/api.scell.io/backend/storage/logs/reverb.log
# 3. Le proxy nginx transmet-il l'upgrade ? (404 = `location /app/` absente)
curl -si --http1.1 --max-time 5 -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
-H "Origin: https://app.scell.io" \
"https://api.scell.io/app/${REVERB_APP_KEY}?protocol=7" | head -n 1Un pusher:error code 4009 apres connexion = origine refusee : ajouter l'hote du dashboard a REVERB_ALLOWED_ORIGINS (hote, pas URL). Provisionnement complet : section "Serveur WebSocket Reverb".
Erreur de migration
Symptome : SQLSTATE[42P07]: Duplicate table ou SQLSTATE[42P01]: Undefined table.
Solution :
# Verifier l'etat des migrations
php artisan migrate:status
# Si une migration est bloquee, ne JAMAIS la modifier en production
# Creer une nouvelle migration corrective a la placeProbleme de certificat SSL
Symptome : cURL error 60: SSL certificate problem
Solution :
# Verifier les certificats
sudo certbot certificates
# Renouveler manuellement si necessaire
sudo certbot renew --force-renewal
# Verifier la configuration Nginx
sudo nginx -t
sudo systemctl reload nginxSolde insuffisant pour un tenant
Symptome : Erreur 402 "Solde insuffisant" lors de la creation de facture.
Solution :
# Verifier le solde du tenant (via Tinker)
php artisan tinker
>>> Tenant::find('uuid-du-tenant')->balance
# 0.00
# Crediter manuellement (admin)
curl -X POST https://api.scell.io/api/v1/admin/users/{userId}/credit \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{ "amount": 100.00, "reason": "Recharge manuelle" }'Redis saturee
Symptome : MISCONF Redis is configured to save RDB snapshots
Solution :
# Verifier l'utilisation memoire
redis-cli info memory
# Nettoyer les sessions MCP expirees
php artisan tinker
>>> App\Models\McpSession::where('expires_at', '<', now())->delete()
# Vider le cache si necessaire
php artisan cache:clearPerformance degradee
Symptome : Temps de reponse > 1 seconde.
Diagnostic :
# Verifier que les caches sont actifs
php artisan config:cache
php artisan route:cache
# Verifier les requetes lentes PostgreSQL
# Dans psql :
SELECT query, calls, mean_time, total_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
# Verifier les index manquants
php artisan tinker
>>> DB::select("SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname = 'public' ORDER BY tablename")FAQ technique
Q : Comment regenerer la cle APP_KEY ?
php artisan key:generate
# ATTENTION : invalide toutes les sessions et tokens existantsQ : Comment ajouter un nouveau worker Horizon ?
Editer config/horizon.php et ajouter un nouveau superviseur dans la section environments.
Q : Comment debugger un webhook SuperPDP ?
Les webhooks sont traces dans la table processed_webhooks. Consulter les logs :
grep "SuperPDP" /home/ploi/api.scell.io/backend/storage/logs/laravel.log | tail -20Q : Comment forcer une cloture fiscale journaliere ?
php artisan fiscal:daily-closing --forceQ : Ou sont stockes les fichiers (factures PDF, documents KYB) ?
En production, les fichiers sont sur Scaleway S3 (bucket scell-storage). En local, ils sont dans backend/storage/app/.
Q : Comment acceder a la base de donnees en production ?
ssh ploi@51.15.234.246
psql -h rdb-xxx.scw.cloud -U scell -d scell_production