Config Sync Rules¶
Rules and pitfalls for VPN config synchronization. Derived from the Felis incident (January 2026).
The Felis Incident (20 Jan 2026)¶
A user received only 26/49 configs (53%) because:
- 46 servers were deactivated on Jan 17 — no resync was triggered
- On Jan 20, the backend returned partial data (26/49) and the desktop client cached it for 1 hour
- Background sync completed in ~8 min, but the cache already held stale data
- User experienced a crash ~2.5 hours later when attempting to use VPN
Key timeline:
11:42 — CONFIG_FROM_DB_INCOMPLETE: healthyConfigs=26 activeServers=49
11:42 — Background sync started for 23 missing servers
11:50 — CONFIG_FROM_DB_SUCCESS: 27 countries, 49 servers ✓
12:00 — XUI panel errors, MySQL connection pool issues
14:27 — User tries VPN → app crashes
14:39 — After restart: SUCCESS (27/49)
Client Cache TTL¶
| Client | Cache TTL | Notes |
|---|---|---|
| Desktop (Rust) | 1 hour | shiva-api/src/rest/relay.rs — CACHE_HOURS = 1 |
| Android | Room DB | Updated on each request |
| iOS | FileManager | Updated on each request |
Desktop caches for 1 hour
If a user gets partial configs, the desktop app will use them for up to 1 hour even after the backend has recovered. A restart clears the cache.
Rule 1: After Server Deactivation — Trigger Resync¶
When deactivating servers in bulk, always clean up stale configs:
-- Option 1: Reset sync status for affected accounts
UPDATE vpn_user_server_account
SET status = 'PENDING'
WHERE server_id IN (SELECT id FROM server WHERE is_active = 0);
-- Option 2: Delete stale configs (forces rebuild on next request)
DELETE FROM vpn_user_server_config
WHERE server_id IN (SELECT id FROM server WHERE is_active = 0);
After running, wait ~10 minutes for background sync to complete.
Rule 2: Partial Data Response¶
Current behavior (problematic):
When healthyConfigs / activeServers < threshold, the backend returns partial data and starts background sync. The client receives and may cache an incomplete list.
Recommendation: If ratio < 0.8, return an error or empty response — let the client use its old cache rather than caching the partial response.
File: backend-app/.../CountryServiceImpl.java:808-820
Dependency Checklist¶
Before activating a server:
-
server.transport_paramsfilled (check:SELECT transport_params FROM server WHERE id = X) - Clients exist on XUI panel (check via SQLite on the VPN server)
-
server.is_active = 1set only after above are confirmed - For whitelist servers:
transport_paramsMUST be pre-filled (no fallback to XUI HTTP)
After deactivating servers:
- Run resync query (see Rule 1 above)
- Wait ~10 min
- Verify ratio:
SELECT COUNT(*) FROM vpn_user_server_config c JOIN server s ON c.server_id=s.id WHERE s.is_active=1vs total active servers
What Works / What Doesn't¶
Works: - Background sync recovers missing configs within ~8 minutes - After app restart, fresh data is loaded - Config pipeline continuously syncs (rolling, ~2 min per server)
Doesn't work: - No mechanism to force-invalidate desktop client cache - Backend returns partial data instead of error when ratio is low - No automated trigger for resync after bulk deactivation
Verifying Config State¶
# Count configs per account vs active servers
./scripts/ssh-internal.sh 10.99.87.62 "docker exec vpn-db mysql -uvpn -p'$(см. Vaultwarden → Databases)' vpn -e \
\"SELECT
COUNT(*) as total_configs,
SUM(CASE WHEN s.is_active=1 THEN 1 ELSE 0 END) as active_configs,
(SELECT COUNT(*) FROM server WHERE is_active=1) as total_active_servers
FROM vpn_user_server_config c
JOIN server s ON c.server_id = s.id
WHERE c.account_id = UNHEX('ACCOUNT_UUID_HEX')\""
# Check accounts missing configs entirely (backend's background sync trigger)
./scripts/ssh-internal.sh 10.99.87.62 "docker exec vpn-db mysql -uvpn -p'$(см. Vaultwarden → Databases)' vpn -e \
\"SELECT COUNT(DISTINCT a.id) FROM account a
WHERE a.deleted_at IS NULL
AND a.pay_status NOT IN ('POOL')
AND NOT EXISTS (
SELECT 1 FROM vpn_user_server_config c WHERE c.account_id = a.id
)\""
Related¶
- Config Delivery Flow — full pipeline description
- VPN Config Service — worker architecture