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.
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,sessionidandcsrftoken. 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-platformnor theapp-versionheader.
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 withUser 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>.orYour 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:
- 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. - 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.1and theUpgradeandConnectionheaders). 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,sessionidandcsrftokenon 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
Originheader 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 -1Maintenance
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.
Store links on your website
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.