MMashDiv

Server settings that shape the app

Everything on your Bicrypto server that changes what the app does — how the server recognises it, sign-in and session lifetimes, the settings it reads, KYC, news, coin art and uploads, WebSockets through your proxy, maintenance, rate limits, the mobile-app switch on the AI Market Maker, store links and the legal pages.

14 min readUpdated 26 September 2026admin, server, settings, sessions, kyc, news, websockets, nginx, maintenance, rate-limits

The app has no admin screens. Everything it shows comes from your server: prices, balances, which features exist, the news feed, coin art, the legal pages and every refusal. There is also no single "mobile app" page in the admin. The settings that affect the app are spread across Platform Settings, Compliance, Communication Tools and one switch on the AI Market Maker page. This page collects them, and says which ones the app ignores.

Where What it changes in the app
System → Platform Settings (/admin/system/settings) Sign-in rules, KYC enforcement, licence attestations, AI Investment and MLM, fiat wallets, the spot deposit flow
System → Compliance → Licence Attestations (/admin/system/attestation) Which regulated features reach app users in which countries. See Licence attestations
System → Communication Tools News sources, the Home announcements, the push tester
Users → Compliance & Verification → Verification Levels (/admin/crm/kyc/level) Which actions each KYC level allows, on the website and in the app
AI Market Maker settings (/admin/ai/market-maker/settings) This platform ships a mobile app — see below
Your server's .env Session lifetimes, news keys, push credentials, rate limits, proxy trust
Your reverse proxy WebSockets, custom headers, upload size

How your server recognises the app

Every HTTP request the app sends carries the header platform: mobile, and so does the upgrade of each signed-in WebSocket. The five public market sockets (tickers and market data) are opened without it. The server treats a request as coming from a native app when platform is one of app, ios, ipados, android, mobile or native (case and surrounding spaces ignored). It does not look at the User-Agent.

That header changes how the server answers:

  • Sign-in credentials come back in the response body, not as cookies. The app stores them and sends them back as the headers accesstoken, sessionid and csrftoken. Renewed credentials come back as response headers.
  • Forward-looking figures are removed from responses: APR and APY, projected or estimated returns, and maximum leverage. Realised profit and loss and earned amounts are kept. Unsafe HTML is stripped from article text such as news and FAQ answers. A customer comparing the website and the app sees these figures on the website only. This is by design, because app stores treat promised returns as a policy problem.
  • Store-policy refusals apply. Some products are refused to the app with a 403 whatever your settings say. See What the server refuses to the app.
  • The licence attestation gate applies, when you have switched it on.
  • Active Sessions lists an app session as Mobile app, with no version, because the app sends neither the client-platform nor the app-version header.

The header is a label, not a credential. Anyone can send it. Identity always comes from the session tokens, so the header cannot sign anyone in or skip a permission check.

platform, accesstoken, sessionid and csrftoken must reach the backend unchanged, on ordinary requests and on WebSocket upgrades. A CDN, firewall or proxy rule that strips unknown headers makes every signed-in request fail with Authentication Required, which signs app users out, while the website (which uses cookies) keeps working.

Signing in and staying signed in

The app signs in with POST /api/auth/login/flutter (email and password), and with POST /api/auth/login/google for Continue with Google. It is the same account, the same password and the same rules as the website's sign-in, with these differences and settings worth knowing:

  • No captcha on sign-in. The app's sign-in route asks for none, so your captcha provider never blocks sign-in. It does apply to sign-up and password reset; see Sign-in methods.
  • Email Verification Required (verifyEmailStatus, Security → Authentication) refuses an unverified address with User email not verified. Verification email sent. A missing settings row counts as on.
  • Two-Factor Authentication (twoFactorStatus, Security → Two-Factor Authentication) must be on for an account's own two-factor to be asked at sign-in, by password or by Google. The app then shows its code screen, which accepts a six-digit code or, behind Can't get a code? Use a recovery code, one of the account's recovery codes.
  • Five wrong passwords lock the account for five minutes after the last failure: Too many failed login attempts, account temporarily blocked.

Session lifetimes

Two environment variables decide how long a sign-in lasts:

Variable Default What it is
JWT_EXPIRY 15m How long one access token is valid
JWT_REFRESH_EXPIRY 14d How long the session behind it lives

Customers never see the 15-minute expiry. When a request arrives with an expired access token, the server issues a new one from the session and sends it back in the response headers, and the app stores it. The app never holds a refresh token: the session ID it keeps is what the server renews from.

Using the app does not stretch a session past JWT_REFRESH_EXPIRY. The session ends that long after sign-in, however active the customer has been, and the customer signs in again.

When a session ends

A session also ends when the customer signs out, resets their password or deletes their account; when an admin blocks, bans, suspends or deletes the account, changes its email or role, or turns off its two-factor; or when Redis loses the session keys. Changing APP_REFRESH_TOKEN_SECRET signs everyone out as well, because no existing session can be renewed under the new secret. Changing APP_ACCESS_TOKEN_SECRET alone does not: the next request renews the access token from the session.

The app decides "signed in" from what it stored, without asking the server when it opens. A session that ended while the app was closed is found at the first request, and the app then returns the customer to the sign-in screen with Your session has ended. Please sign in again. When the server signed the customer out for a stated reason, such as too many incorrect codes, the app shows that reason instead. A wrong password, PIN or code typed on a screen is not a session ending: that screen shows the error and the customer stays signed in. Troubleshooting has the checks for customers signed out unexpectedly.

The settings the app reads

The app loads GET /api/settings at start-up and again every five minutes while it is open, and keeps the last copy on the phone. With no connection it uses that copy, however old. The route needs no sign-in and returns every settings row except server-only secrets (the captcha secret key, the geo lookup API key and internal pool-backing state).

Only a few of those settings change the app itself:

Setting Where Effect in the app
Fiat Wallets (fiatWallets) Platform Settings → Wallet → Wallet Types Off hides fiat from the deposit screen and the wallet overview
Spot Deposit Attribution (spotDepositMode) Platform Settings → Wallet → Transactions Chooses the spot deposit flow: paste a transaction hash (hash_claim, the default), send an exact amount (amount_match), or deposit to the customer's own address (ecosystem_custody). See Spot deposit modes
E-commerce shipping and tax (ecommerceShippingEnabled, ecommerceDefaultShippingCost, ecommerceTaxEnabled, ecommerceDefaultTaxRate) E-commerce admin → Settings (/admin/ecommerce/settings) The shipping and tax lines of the Store cart summary, worked out as checkout charges them: each is on only when its switch is saved as true, a setting never saved counts as off, the tax rate is a percentage, and shipping is one flat charge per cart with physical products in it. There is no free-shipping threshold. Until the settings have loaded, the cart says Shipping and tax are calculated at checkout

The rest are published to the app and not read by it: the logos, the theme and layout switchers, the deposit, withdrawal and transfer switches, the social links, the store links and the minimum-version settings. The app's name, colours and icon come from the build, not from your server; see Branding. Anything your server enforces on its own routes still applies to app requests.

Which screens a customer sees

At every sign-in, and each time it starts with a saved sign-in, the app asks GET /api/user/modules which features this customer may use, and shows only those. The answer combines whether the add-on is installed, switched on and licensed, the customer's KYC level and, when you enforce it, their country's licence attestations. The website is not driven by this list. The module manifest explains each reason and the message the app shows for it.

KYC

Two switches under Platform Settings → Features → Verification decide whether KYC levels limit anything:

Setting Default Effect
KYC Verification (kycStatus) on KYC is offered at all
Enforce KYC Feature Access (kycFeatureEnforcement) off Enforces the feature switches on each level in Users → Compliance & Verification → Verification Levels (/admin/crm/kyc/level)

Only with both on do a level's feature switches restrict anything, and then they do so on the website and in the app alike. A few add-on doors keep a KYC check of their own that asks for any approved application while KYC Verification is on, whatever Enforce KYC Feature Access says: merchant registration when the payment gateway's own KYC setting is on, and a trade against a P2P offer whose maker requires KYC. In the app:

  • Whole features are held back with the message "Complete identity verification to use Feature." when the customer's level lacks the feature's KYC switch: Spot trading (trade), Token offerings (purchase_ico), Store (view_ecommerce), Futures (futures_trading), Merchant (use_gateway) and AI Investment (invest_ai). The Wallet tab always shows.
  • Individual actions are refused by the server with a 403, even inside a feature the customer can open: spot orders, deposits, withdrawals, transfers between wallets, opening a support ticket, staking, P2P offers and trades, blog comments, and each add-on's own actions. The message is KYC verification is required to <action>. or Your verification level does not include this feature (<action>). Complete a higher verification level to continue.

With Enforce KYC Feature Access on, a customer whose level does not list a feature is refused that action everywhere. A level that does not list the deposit feature (deposit_wallet) or the support-ticket feature (support_ticket) refuses those to every customer on it. See KYC.

The country on a customer's approved KYC application is also the first source of their country of residence for licence attestations.

The app uploads KYC documents to POST /api/upload/kyc-document (one file of up to 10 MB per upload). They are stored privately and are never served from /uploads/.

News

The app's news screens read GET /api/news and GET /api/news/article/{id} on your server. Nothing goes from the phone to a news provider, and the app carries no news key. /api/news needs no sign-in.

Your server fills it from, in order:

  1. Your own feed. Stories from the providers you enable under System → Communication Tools → News Providers (/admin/system/news/provider), plus the stories you write under Market News (/admin/system/news). Only active stories published in the last 30 days are served, newest first.
  2. A CryptoCompare proxy, only when your own feed has nothing, using the server's APP_CRYPTOCOMPARE_API_KEY.

With neither, the app's news screen shows the server's refusal: News is not configured on this deployment. Enable a provider under Admin -> System -> News and run a sync ...

Provider Key
Finnhub APP_FINNHUB_API_KEY
CryptoCompare APP_CRYPTOCOMPARE_API_KEY
CryptoPanic APP_CRYPTOPANIC_API_KEY
RSS none

A key can also be stored on the provider in the News Providers screen, where it overrides the environment variable. Providers are synced on a schedule, and the same screen can run a sync on demand. The website's trading terminal reads the same feed, so one setup serves both.

Coin logos, images and uploads

The app turns every relative image path it receives (a coin icon, an avatar, a product image) into a full address on the host in your baseUrl. So all images come from that host.

Coin logos. For each coin the app tries the icon path your API gives, then /img/crypto/<symbol>.webp (the symbol in lower case, letters and digits only). If every address fails, it draws a tile with the symbol's letters. The files are the website's public/img/crypto folder. Your backend serves them at /img/crypto/ and at /api/img/crypto/, in lower case, as .webp or .png.

Set baseUrl to the host whose / serves your website, for example https://exchange.example.org. With the standard nginx layout that host serves /img/crypto/, /uploads/ and /api/ together. A host that passes only /api/ to the backend cannot answer /uploads/ or /img/crypto/, and the app then shows missing images and letter tiles.

/uploads/ is served by the backend from frontend/public/uploads/, limited to common image, video and document types. KYC documents are never served from there.

What the app uploads: KYC documents (/api/upload/kyc-document) and P2P dispute evidence (/api/p2p/trade/{id}/dispute/evidence). Both travel as base64 inside JSON, which is larger than the file. Dispute evidence can reach 36 MB per request, so nginx's client_max_body_size must be above that; the core guide uses 40m. See nginx: Uploads.

WebSockets and your proxy

Everything live in the app is a WebSocket to wsBaseUrl plus a path under /api/: prices, charts, order books, open orders, deposit confirmations, support replies, notifications and the Home announcements. Eleven paths are used; the API reference lists them.

  • Your proxy must pass WebSocket upgrades on /api/ (proxy_http_version 1.1 and the Upgrade and Connection headers). See nginx: WebSockets. Without them the market list loads, because it is an ordinary request, but prices stay at zero and nothing updates live.
  • Signed-in sockets authenticate with headers, not cookies. The app sends platform, accesstoken, sessionid and csrftoken on the upgrade. A proxy that strips them makes those sockets fail and retry quietly.
  • Geo restrictions are checked on upgrades too. A blocked country gets a 403 on the socket as it does on ordinary requests.
  • CORS needs no configuration for the app. It sends no Origin header and never makes a preflight request.

Check the upgrade from a computer outside your server. The first line must be HTTP/1.1 101 Switching Protocols:

# --http1.1: over HTTPS curl would otherwise negotiate HTTP/2, where a
# WebSocket upgrade cannot happen, and report a failure that is not one.
curl --http1.1 -i -N -o - -s \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://exchange.example.org/api/exchange/ticker | head -1

Maintenance

There is no maintenance switch in the admin. pnpm stop stops the platform and starts a small maintenance server on its ports, and pnpm start removes it again (see Updating). While it runs, every /api/ request gets HTTP 503 with Retry-After: 300 and the body {"status":false,"message":"Service temporarily unavailable. Maintenance in progress.","statusCode":503}.

What the app shows:

  • When its market-list request fails with a 503, at start-up or on a refresh, a banner across the top of every screen reads Server is under maintenance. Using offline mode. The futures screen's market header can raise the same banner.
  • When it cannot reach the server at all at start-up, the banner reads Unable to connect to server. Using offline mode.
  • Other screens show their own request errors.
  • The banner goes away the next time the market list loads successfully.

The app keeps the market list in memory only. After a cold start during maintenance it has no prices to show, and it never shows invented ones.

Rate limits and the client's address

Every limit is counted per client IP address:

Limit Default Refusal
Any POST, PUT, PATCH or DELETE RATE_LIMIT (100) per RATE_LIMIT_EXPIRE or RATE_LIMIT_EXPIRY seconds (60) 429 Rate Limit Exceeded, Try Again Later
App sign-in 30 per 15 minutes Too many login attempts. Please try again in a few minutes.
Registration 3 new accounts per hour Too many accounts have been created from this address. Please try again later.

Reads (GET) are not counted by the general limit, and neither are WebSocket upgrades. Many phones behind one mobile carrier can share an address; the sign-in limit is sized for that.

The server must see each phone's real address. By default it trusts a forwarding header only from a proxy on the same machine (loopback), which is the standard nginx install and needs nothing set. With a proxy or load balancer on another machine, list its network in TRUST_PROXY_CIDRS. Otherwise every customer shares the proxy's address and one bucket, and sign-ups and sign-ins start failing for everyone. The backend warns about this in its log under RATE_LIMIT when most requests resolve to the same address. TRUST_PROXY=true trusts a forwarding header from any caller; use it only when the API port cannot be reached except through the load balancer. See nginx: Client IP.

This platform ships a mobile app

AI Market Maker settings (/admin/ai/market-maker/settings), Trading tab, Status group. Key mobileAppEnabled, off by default. Its description on screen:

Turn this on if you publish the iOS or Android app. It permanently disables the AI market maker: an app store treats synthetic order-book depth as deceptive, and that finding removes a live app rather than rejecting a new one. You cannot run both.

With it on:

  • The AI market maker refuses to start. The backend logs Engine refused to start: this platform ships a mobile app (mobileAppEnabled). ... No permission or other setting overrides this.
  • Early staking withdrawals no longer wait for approval. A customer who leaves a staking position before its term ends is paid out on their own request, with the pool's early-withdrawal fee still charged, whatever your staking approval settings say. This applies on the website as well. A request already waiting for approval is settled when the customer asks again.

The app does not read this switch. It lives on the AI Market Maker settings page, so an install without that add-on has no way to set it and it counts as off.

AI Investment and MLM

Allow AI Investment & MLM in the Mobile App (mobileAllowRestrictedProducts, Platform Settings → Features → Verification, off by default) lets app users reach AI Investment and the referral programme. The app shows those screens only if it was also built with STORE_RESTRICTED_MODULES=true, so both are needed. App stores commonly reject apps that carry either product; see Restricted modules before you turn it on.

While it is off, the list of features the app loads marks both as unavailable, and the server refuses the app's requests with a 403:

  • AI Investment's plan and investment routes: AI Investment is not available in the mobile app. It offers a fixed return over a fixed duration funded from a custodial wallet, which app-store rules on financial products do not permit.
  • Claiming a referral reward: The multi-level referral programme is not available in the mobile app. App-store rules bar a cryptocurrency app from offering currency for recruiting other users.

The website is unaffected either way. Binary options, forex investment plans and launching a token stay unavailable in the app whatever this is set to.

Licence attestations

Enforce Licence Attestations (Mobile App) (attestationEnforcement, Platform Settings → Features → Verification, off by default) limits nine regulated features in the app to residents of the countries where you have recorded a licence. It never affects the website. See Licence attestations.

Push notifications

Push needs the same Firebase project in the app build and in your server's .env (FCM_PROJECT_ID, FCM_CLIENT_EMAIL, FCM_PRIVATE_KEY, or FCM_SERVICE_ACCOUNT_PATH). These are environment variables, not admin settings. The app registers each signed-in phone with POST /api/user/push/subscribe, sending the push token and a deviceId that is different for each account on that phone. With the deviceId, your server records the phone in its device registry and switches push on for a customer who has never set the preference; it never switches it back on for one who turned it off. Sign-out sends the same ID in the client-device-id header of POST /api/auth/logout, which revokes that phone's record. The tester is under System → Communication Tools → Notification Service. See Push notifications.

Home announcements

The slider on the app's Home screen shows the active rows from System → Communication Tools → System Announcements (/admin/system/announcement), newest first. They arrive over the signed-in WebSocket /api/user, so the slider stays empty when that socket cannot connect.

Platform Settings → Social & Links → Mobile Apps holds App Store Link (appStoreLink) and Google Play Link (googlePlayLink), both empty by default. Your website's home page shows its mobile-app section once either link is set, or while that section's Coming Soon announcement is switched on in the home page editor; the announcement replaces the store buttons. The app itself does not use these links.

The same group holds Minimum iOS App Version (mobileMinVersionIos) and Minimum Android App Version (mobileMinVersionAndroid). They are published to the app and not read by it: no build of the app compares its version with them, so they cannot force anyone to update. Leave them empty.

Terms of Service and Privacy Policy

In the app, Profile → Legal → Terms of Service and Privacy Policy (the gear icon at the top right of Home opens Profile) load GET /api/content/default-page/terms and /privacy. That is the same text as your website's default legal pages. You edit it under Content → Appearance & Design → Default Pages (/admin/default-editor), which the menu shows only while your landing page type is DEFAULT. If you never saved these pages, the server returns the template's built-in legal text, and the app shows that.

Where an install that uses the page builder edits these two pages has not been confirmed. Check what the app shows before you submit it.

The stores also ask for a privacy policy URL. The in-app page is not one; see Your own keys and URLs.

Reports from app users

The app lets customers report blog comments and support tickets. Reports land in Content → Reported Content (/admin/moderation/report). Reporting hides nothing; each item stays up until someone rules on it. Reports about P2P traders go to the P2P admin (/admin/p2p/report).

What it deliberately does not do

  • No administration in the app. The app calls no admin route. Staff accounts use it like customers, and every admin screen stays on the website.
  • No remote configuration. baseUrl, wsBaseUrl, the name and the colours are packed into each build. Your server cannot change them, and moving your backend to a new domain needs a new build, a store update, and the old host kept answering until customers have updated.
  • No forced updates. Old builds keep working until customers update from the store, so keep your server compatible with them.
  • No maintenance flag. The app learns about maintenance only from failed requests.