Scheduled and background work: what actually runs
The supplied PHP application has no cron entry point, job scheduler or durable queue worker. Do not
install a fictitious VIP-expiry cron. PHP handles the work below during requests; Node.js is the
continuously running Socket.IO/Redis relay. There is no automatic offline replay of missed socket
messages.
Keep the real-time relay alive
Use the production process-management guide for
PM2 startup, explicit Node .env loading, logs, restarts and reboot recovery. PHP and Node must share
Redis connection settings and API_KEY. Node does not read .env automatically, and SOCKET_BIND_IP does
not bind its listener; follow the environment and firewall steps.
Troubleshooting with errors, ordered diagnosis and proof
The literal app errors below are taken from the supplied handlers; infrastructure messages are
representative examples whose paths and wording vary by host. They are not claimed to be logs captured
from your production server. Keep tokens, keys and personal data out of support attachments.
1. API responds with HTML, 404 or a JSON parsing error
FormatException: Unexpected character (at character 1)
<!DOCTYPE html>
{"code":404,"err":["Controller ... not found."],"data":null}
- Inspect the response body and exact URL before debugging Flutter models.
- Confirm APPCHAT_API_BASE_URL is the public base directory; the client appends api/. Avoid
/api/api/. Confirm APP_BASE_PATH matches your deployment subdirectory.
- Verify Apache rewrite/Nginx front controller points to public/index.php; static HTML and admin
URLs are not mobile API URLs.
- POST {} to api/helper/config with Content-Type: application/json and API-KEY.
Fixed when: the response is JSON with code=200 and data.agora/socket_ip/socket_port,
then the rebuilt client reads the same values.
2. Login/token rejected
{"code":401,"err":["invalid_token"],"msg":["invalid_token"],"data":null}
loginFailed: {"message":"Invalid API key"}
- Distinguish the API-KEY from the mobile Auth-Token and the separate Admin-Token.
- Compare build-time API key with PHP API_KEY and Node API_KEY. Rebuild the app after changing
dart-define.
- Sign in again; a new device login invalidates old tokens. Check device_id matches the login
device.
- If PHP works but Node says Invalid token, check both services' Redis configuration, then call
auth/tokenLogin to repopulate the Redis mapping before socket conn.
Fixed when: auth/tokenLogin returns code=200, socket receives appCon=1, and no
loginFailed/forceLogout follows unexpectedly.
3. Database connection/import failure
SQLSTATE[HY000] [1045] Access denied for user ...
SQLSTATE[42S02]: Base table or view not found: 1146 ... user_vips ...
- Read PHP/web-server logs. An invalid_token response can mask a database failure because token
lookup catches database exceptions.
- Verify DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME and DB_PASSWORD in PHP .env (mapped internally
to LEGACY_SQL_* keys) and that the service is running.
- Use the same account to connect to the intended database; check grants.
- For missing tables, compare against appchat_clean_install.sql. Import into a new empty database;
never reimport the clean installer over live data.
Fixed when: helper/config and authenticated user/index work and expected tables
exist in the same database PHP uses.
4. Redis/Socket.IO offline or 502
Redis Client Error Error: connect ECONNREFUSED 127.0.0.1:6379
Redis subscriber Error ...
WebSocket connection ... failed: Error during WebSocket handshake: Unexpected response code: 502
- Run pm2 status and inspect the relay logs. Resolve startup errors before changing Flutter.
- Check Redis PING on the configured host and credentials; do not print passwords in tickets.
- Check Node PORT against the reverse-proxy upstream and allow only the required private
connectivity.
- Verify HTTPS certificate, socket hostname, /socket.io/ proxy path and Upgrade/Connection
headers.
- Reconnect using conn with a valid token, then enter the room through PHP.
Fixed when: logs show redis connect Success, appCon=1 arrives, and two devices
exchange roomMessage. PHP success alone does not confirm real-time delivery.
5. Room join crashes or produces no seat/chat updates
TypeError: Cannot set properties of undefined (setting 'users_count')
{"code":200,"data":{"state":"bad_password","err":"bad_password"}}
- For the TypeError, inspect the joinRoom payload: this server dereferences data.user. A direct
joinRoom must include user:{} or the authorized user resource.
- Avoid conn.rid in this version because its internal join call omits user; authenticate first,
then POST live/enterRoom.
- Check data.state, room password and membership restrictions before treating code=200 as entry
success.
- Check the PHP→Redis update channel and Node subscription, then re-enter through PHP after
reconnect.
Fixed when: enterRoom reaches room listeners and subsequent seat actions produce a
roomSeats array. Do not send seatTake or sendGift as socket events.
6. Agora room opens but audio fails
room.user_sw_token: ""
# Representative SDK symptom: token invalid / token expired / microphone permission denied
- Check helper/config data.agora, then room.agora_channel, agora_uid and user_sw_token returned by
live/enterRoom.
- An empty token means the backend Agora app ID/certificate is absent or still a CHANGE_ME
placeholder.
- Use credentials from the same Agora project and the exact channel/UID used to mint the token;
confirm clocks and TTL.
- Grant microphone permission on both devices; on iOS add NSMicrophoneUsageDescription before
building.
Fixed when: two physical devices join the same room, take seats and hear each other;
repeat after leaving and re-entering.
7. Recharge purchased but wallet unchanged
no_charge_rule
repeated_charge
invalid_payment_order
- Read payment/pay's JSON error and payment_logs status/error_text for the purchase. Do not grant
coins merely because the client reported a purchase.
- For no_charge_rule, match product_id to the active recharge_packages Android/iOS product ID.
- Check provider, receipt field, package/bundle ID, service account/API permissions and platform
tester account.
- For repeated_charge, look for the already credited order, refresh cash/index and compare
wallet_transactions; duplicate verification is not a second credit.
Fixed when: a new sandbox purchase has one successful payment record, exactly one
expected wallet credit, and the app shows the new balance.
8. Gift/store/VIP rejected
not_enough_coins
item_not_found
vip_not_owned
vip_expired
- Refresh cash/index and the relevant gift/store/VIP catalogue.
- Use the catalog ID for purchase and the owned inventory ID for equip. Check state=9 and correct
recipient.
- For gifts calculate unit cost × count × recipient count; backpack sends require enough owned
quantity.
- For VIP compare user_vips.expires_at with UTC_TIMESTAMP(); use an owned unexpired package when
activating.
Fixed when: the single intended transaction succeeds, balance changes once, and
inventory/VIP refresh agrees with the result.
9. Upload fails or image is broken
Invalid upload token
Unsupported upload type.
Bunny storage is not configured.
Upload too large
- Obtain a fresh upload plan and keep the entire signed URL; use PUT with binary bytes.
- Check the selected driver, allowed extension, PHP/web-server upload limits and the application's
25 MiB limit (26,214,400 bytes).
- For local storage check writable destination directories; for Bunny check storage region, zone,
key and CDN hostname.
- Open the returned display URL separately. An upload endpoint and its public display URL serve
different purposes.
Fixed when: PUT returns HTTP 200 and OK (server driver), GET displays the saved
file, and a second device loads the image.
10. Moderator cannot see a section or can see too much
Permission denied: reports.manage
Invalid request token
- Confirm the active admin account, role state=9 and assigned roles.
- Add both the view permission and intended action permission. Permissions across roles are
combined; remove unintended broad roles.
- Sign out and back in. For CSRF errors reload the form in that session and submit the current
_csrf value.
- Test both the sidebar and the direct restricted page URL.
Fixed when: allowed operations work, restricted page/action requests fail, and the
audit log records the permitted change.
11. VIP/level/ranking seems stale
# Example symptom, not a server exception:
VIP badge remains visible after expires_at; profile/ranking has old points.
- Compare UTC database time and expiry/transaction timestamps.
- Refresh auth/tokenLogin for the affected user and fetch ranking again with the intended
type/time/rid.
- Confirm the underlying gift/payment transaction committed and the user point totals changed.
- If totals differ, preserve records and reconcile on a backup; no cron or replay worker is
included.
Fixed when: expired VIP becomes null, level matches the configured threshold and
ranking reflects transactions in its UTC period.
12. Flutter/iOS configuration or signing failure
[core/no-app] No Firebase App '[DEFAULT]' has been created
No profiles for 'com.example.appchat' were found
Signing for "Runner" requires a development team
- Run flutter doctor -v on the Mac; install the required Xcode components.
- Register your actual bundle ID in Firebase and Apple, add the iOS Firebase plist to the Runner
target, and regenerate firebase_options.dart.
- Open Runner.xcworkspace, select the Runner target and correct team, then enable automatic
signing or install matching certificates/profiles.
- Build a development version on a physical device before archiving a release.
Fixed when: the device build launches and signs in, release archive validates, and
App Store Connect processes the uploaded build.