MEF Web and Android — Installation guide
Version 1.11.0 · Ubuntu · Supabase · Installation licensing
This guide is for customers installing the source package on their own infrastructure. Shell commands are for an Ubuntu Bash session over SSH. Android builds run on a developer workstation or EAS. Take a backup before changing an existing installation. The new-server instructions must not be applied over an existing Plesk or hosting-panel configuration.
1. Domains and installation addresses
The installer configures the domain once. Application users never enter a server address or a license key. Android connects to the HTTPS API address included at build time.
| Address | Purpose |
|---|---|
| https://app.example.com | Application website and API in this guide |
| https://app.example.com/admin | Application administration |
| wss://rtc.example.com | LiveKit voice, video and livestream signaling |
| https://PROJECT_REF.supabase.co | Your Supabase Auth, database and storage |
| https://license.voxexo.com | Vendor license service; retain this value unless the vendor instructs otherwise |
example.com, PROJECT_REF and 203.0.113.10 are documentation placeholders. The IP is reserved for examples and is not a working server. Replace it with your server's public IP. 127.0.0.1 is the server's internal loopback address and must remain internal.
If API and website use different domains, set EXPO_PUBLIC_API_URL and LICENSE_INSTALLATION_ORIGIN to the API origin, include the website origin in CORS_ORIGINS, and use the website URL for Supabase Auth Site URL. Web and Android share the same API and database.
2. Source packages and deliverables
| Package | Contents |
|---|---|
| MEF-Web-Source.zip | Web frontend, API, SQL migrations, deployment templates and documentation |
| MEF-Android-Addon.zip | Native source overlay requiring the matching Web source version and Android entitlement |
| MEF-Web-Android-Source.zip | Combined Web and Android sources |
| APK | Installable Android test/build output; not editable source |
| AAB | Google Play upload output; not editable source |
Extract the source so that frontend/, server/, supabase/, deployment/ and documentation/ are siblings. Do not apply the Android overlay again when using the combined package. Dependencies, SDKs, Gradle caches, AI model weights, production user data, private environment files and signing secrets are excluded. npm ci downloads dependencies from the lockfile; the installed project is larger than its source ZIP.
3. Requirements
- An Ubuntu server, SSH access and permission to manage services.
- A domain with DNS access and valid HTTPS certificates.
- Node.js 24 LTS and npm; verify
node --version,npm --versionandcommand -v node. - Your own Supabase project, public project key and server-only service_role key.
- A verified application operator email and an installation license beginning with
VOX-. - SMTP for confirmation and password reset emails.
- LiveKit and reachable media ports for voice, video and livestreams.
- For Android: a unique package name, Android SDK/JDK or an EAS account, and your own release/upload signing key.
Supabase account credentials, the project database password, the application account password and the installation license key are different credentials. Never substitute one for another.
4. Configuration file reference
| File or dashboard | Field | Value or example |
|---|---|---|
| frontend/.env | EXPO_PUBLIC_API_URL | https://app.example.com — no /v1 suffix |
| frontend/.env | EXPO_PUBLIC_SUPABASE_URL | https://PROJECT_REF.supabase.co |
| frontend/.env | EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Your project's public publishable/anon key |
| /etc/mef/api.env | SUPABASE_URL, SUPABASE_ANON_KEY | Same project URL and public key |
| /etc/mef/api.env | SUPABASE_SERVICE_ROLE_KEY | Private server-only service_role key |
| /etc/mef/api.env | CORS_ORIGINS | https://app.example.com; exact origins separated by commas |
| /etc/mef/api.env | LICENSE_INSTALLATION_ORIGIN | https://app.example.com; no trailing slash |
| /etc/mef/api.env | LICENSE_ISSUER | https://license.voxexo.com |
| /etc/mef/api.env | LICENSE_PUBLIC_KEY_FILE | /opt/mef/server/license-public.pem |
| /etc/mef/api.env | LICENSE_STATE_FILE | /var/lib/mef-license/installation.json |
| frontend/app.json | expo.name, slug, scheme | My App, my-app, myapp |
| frontend/app.json | expo.android.package, versionCode | com.example.myapp, 1; increment for store updates |
| frontend/tools/finalize-web.mjs | title, description and URL | Your branding and website origin |
| /etc/nginx/sites-available/mef | server_name, root, proxy_pass | app.example.com, /var/www/mef, http://127.0.0.1:3001 |
| Supabase Auth → URL Configuration | Site URL, Redirect URLs | https://app.example.com/ and myapp:// |
| Supabase Auth → Email → SMTP | Server, port, user, password, sender | Your email provider's settings |
| /etc/mef/api.env | LIVEKIT_URL, LIVEKIT_HTTP_URL | wss://rtc.example.com, http://127.0.0.1:7880 |
.env.example is a template. Put actual frontend settings in frontend/.env, and actual server secrets in /etc/mef/api.env. EXPO_PUBLIC_ variables are embedded in the client and can be read by users. Never put a service_role key, license signing private key, SMTP password or bank encryption key there.
5. DNS and HTTPS
Create DNS A records for app and, when using LiveKit, rtc, pointing to your public server IP. For example, app → 203.0.113.10 illustrates the format only. Remove incorrect AAAA records if IPv6 is not configured.
getent ahosts app.example.com
getent ahosts rtc.example.comUse HTTPS in production. A phone's localhost refers to the phone, not your Ubuntu server. License activation requires a publicly reachable HTTPS domain. If a CDN or firewall protects the application, permit the license service to read /v1/license/challenge/… without an interactive browser challenge. WebRTC media is not carried by an ordinary HTTP CDN proxy; configure the media origin and ports directly.
6. Prepare a new Ubuntu server
For a clean server, install the base tools and web server:
sudo apt update
sudo apt install -y ca-certificates curl unzip python3 openssl nginx certbot python3-certbot-nginxInstall a supported Node.js 24 LTS binary from the official Node.js distribution, verify its published checksum, and record the version. The service examples assume /opt/mef-node/bin/node and npm in the same directory. Adjust that path if Node is installed elsewhere. Do not overwrite an existing hosting panel's Node or Nginx installation.
sudo useradd --system --create-home --home-dir /var/lib/mef --shell /usr/sbin/nologin mef
sudo install -d -o mef -g mef -m 755 /opt/mef
sudo install -d -o root -g root -m 700 /etc/mef
sudo install -d -o mef -g mef -m 700 /var/lib/mef-licenseSkip account creation if the account already exists. Upload and extract the source into /opt/mef using SFTP/SCP. Check the directory structure before setting ownership:
ls /opt/mef/frontend/package.json /opt/mef/server/package.json
sudo chown -R mef:mef /opt/mef/frontend /opt/mef/serverKeep your SSH port permitted before changing a firewall. Website traffic requires 80/TCP and 443/TCP. The API port 3001 remains on loopback.
7. Create the Supabase project and apply migrations
Create your own Supabase project. Obtain the project ref, URL, public key and service_role key from its dashboard. In the source root, inspect the current CLI help and initialize local configuration if supabase/config.toml does not exist:
cd /opt/mef
npx supabase --version
npx supabase init --help
npx supabase link --help
npx supabase db push --helpRun npx supabase init only when local configuration is missing. Then:
npx supabase login
npx supabase link --project-ref PROJECT_REF
npx supabase db push --dry-run
npx supabase db push
npx supabase migration listUse interactive authentication and database password prompts. Apply every SQL migration in filename order; applying only the first schema file is insufficient. Before upgrading an existing database, compare migration histories and back up. Do not resolve history conflicts by running a production database reset.
Read-only checks in SQL Editor:
select to_regclass('public.profiles') as profiles,
to_regclass('public.messages') as messages,
to_regclass('mef_private.operator_emails') as operators;
select id, public from storage.buckets order by id;
select tablename from pg_publication_tables
where pubname = 'supabase_realtime' and schemaname = 'public'
order by tablename;Migrations define storage buckets, access rules and Realtime publication entries. Keep RLS enabled. Do not make private buckets public to bypass an access error. A server-only key must never be shipped in an application build.
8. Authentication, email and administrator access
In Supabase Authentication → URL Configuration, set Site URL to https://app.example.com/. Add https://app.example.com/ and your Android scheme, for example myapp://, to Redirect URLs. The scheme must match expo.scheme in frontend/app.json.
Configure custom SMTP with your provider's host, port, credentials and authorized sender, such as support@example.com. Add the provider's SPF/DKIM records and configure DMARC as appropriate. Test both confirmation and password recovery. The default Supabase mail service must not be assumed suitable for unrestricted production sending.
Register and verify the operator's application account. If onboarding is blocked before license activation, create and confirm the operator using Supabase's supported Auth user management. Do not insert rows directly into auth.users. Authorize the exact verified operator email:
insert into mef_private.operator_emails(email)
values (lower('owner@example.com'))
on conflict do nothing;
select id,email,email_confirmed_at from auth.users
where lower(email)=lower('owner@example.com');Replace the example email with the actual verified operator address. Administrator access is checked by the canonical server/database authorization flow, not by an editable profile field.
9. Configure the private API environment
sudo install -o root -g root -m 600 /opt/mef/server/.env.example /etc/mef/api.env
sudo nano /etc/mef/api.envFill in the fields below. Preserve other template fields that your installation needs:
SUPABASE_URL=https://PROJECT_REF.supabase.co
SUPABASE_ANON_KEY=YOUR_PROJECT_PUBLIC_KEY
SUPABASE_SERVICE_ROLE_KEY=YOUR_SERVER_ONLY_SERVICE_ROLE_KEY
HOST=127.0.0.1
PORT=3001
NODE_ENV=production
CORS_ORIGINS=https://app.example.com
PRIVATE_CHARACTER_TEST_MODE=false
CASHOUT_ENCRYPTION_KEY=YOUR_NEW_64_CHARACTER_HEX_KEY
LICENSE_ISSUER=https://license.voxexo.com
LICENSE_INSTALLATION_ORIGIN=https://app.example.com
LICENSE_PUBLIC_KEY_FILE=/opt/mef/server/license-public.pem
LICENSE_STATE_FILE=/var/lib/mef-license/installation.json
LICENSE_REQUIRED=trueGenerate a new bank-detail encryption key using openssl rand -hex 32. Store it securely in this private configuration and back it up. Once encrypted bank records exist, replacing this key without re-encryption makes old records unreadable. Public and server Supabase keys must belong to the same project used by the frontend.
The public verification PEM is intentionally readable by the API service:
sudo chmod 644 /opt/mef/server/license-public.pem
sudo -u mef test -r /opt/mef/server/license-public.pem
sudo -u mef test -w /var/lib/mef-licenseKeep the private signing key on the vendor's license server. A PEM placed inside a root-only directory remains inaccessible even if the file itself is mode 644.
10. Build and run the API
cd /opt/mef/server
sudo -u mef /opt/mef-node/bin/npm ci
sudo -u mef /opt/mef-node/bin/npm run typecheck
sudo -u mef /opt/mef-node/bin/npm run buildCreate /etc/systemd/system/mef-api.service:
[Unit]
Description=MEF application API
Wants=network-online.target
After=network-online.target
[Service]
User=mef
Group=mef
WorkingDirectory=/opt/mef/server
EnvironmentFile=/etc/mef/api.env
Environment=NODE_ENV=production
ExecStart=/opt/mef-node/bin/node dist/index.js
Restart=on-failure
RestartSec=5
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/mef-license
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now mef-api
sudo systemctl status mef-api --no-pager
curl -fsS http://127.0.0.1:3001/healthA successful health response proves the API started, not that SMTP, storage, camera or payment integrations passed acceptance testing. Do not expose port 3001 to the internet.
11. Configure and build the website
cd /opt/mef/frontend
sudo -u mef cp .env.example .env
sudo -u mef nano .envEXPO_PUBLIC_SUPABASE_URL=https://PROJECT_REF.supabase.co
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_PROJECT_PUBLIC_KEY
EXPO_PUBLIC_API_URL=https://app.example.comDo not add /v1 to the API URL; the client adds route prefixes. Frontend configuration is embedded at build time. Changing .env requires rebuilding the website and Android application; restarting Nginx alone does not update it.
Update your brand in app.json, logos and application brand copy. Update website metadata in tools/finalize-web.mjs. Retain the vendor license service address and the public verification key.
sudo -u mef /opt/mef-node/bin/npm ci
sudo -u mef /opt/mef-node/bin/npm run typecheck
sudo -u mef /opt/mef-node/bin/npm run lint
sudo -u mef /opt/mef-node/bin/npm run export:web
sudo install -d -m 755 /var/www/mef
sudo cp -a dist/. /var/www/mef/
sudo find /var/www/mef -type d -exec chmod 755 {} +
sudo find /var/www/mef -type f -exec chmod 644 {} +Publish only the exported dist contents. Never use the entire source directory as a web document root. Private environments, server source, package caches and development test tools do not belong in the public website.
12. Nginx, HTTPS and /admin
On a new Ubuntu server, create /etc/nginx/sites-available/mef. Replace the example domain and ensure the root matches your export directory:
server {
listen 80;
server_name app.example.com;
root /var/www/mef;
index index.html;
location / { try_files $uri $uri/ /index.html; }
location = /admin {
try_files /index.html =404;
add_header Cache-Control "no-store" always;
}
location = /admin/ { return 308 /admin; }
location ~ ^/(v1(?:/|$)|health$) {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 32k;
proxy_read_timeout 120s;
add_header Cache-Control "no-store" always;
}
location ~ ^/(?:\.env|\.git|node_modules|server|src)(?:/|$) { return 404; }
}sudo ln -s /etc/nginx/sites-available/mef /etc/nginx/sites-enabled/mef
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com
sudo certbot renew --dry-run
curl -I https://app.example.com/admin
curl -fsS https://app.example.com/healthVerify HTTP redirects to HTTPS and automatic renewal is scheduled. On Plesk or another hosting panel, configure its existing domain, document root, SSL and reverse proxy instead of installing a competing listener.
13. Activate the installation license
Sign in as the verified application operator, open /admin, then Installation license, enter the VOX-… key and activate. This is your application's administration panel; customers do not need access to the vendor's seller panel.
If initial profile onboarding prevents access before activation, use the supplied first-install helper. Keep activate-owner.py and activate-owner.mjs together in the documentation/ directory:
sudo python3 /opt/mef/documentation/activate-owner.py /opt/mef/server /etc/mef/api.envEnter the verified application operator email, application password and installation key when prompted. Password and key input is hidden. The helper authenticates normally, checks canonical administrator access and calls the running loopback API. It does not create accounts, grant administrator privileges or disable licensing. A successful response includes active status, the installation origin and product entitlements. Complete onboarding and verify the license from /admin afterward.
The license server verifies the public HTTPS challenge endpoint during activation. A random challenge URL can legitimately return 404. One license is bound to one production installation and origin. Android entitlement is attached to the same installation; no second user database or Android domain is needed. Contact the vendor for an authorized domain reset when moving the installation.
The signed lease refreshes approximately daily and has a maximum seven-day validity for brief connectivity outages. This is a cached entitlement, not a seven-day subscription. Revocation is enforced on refresh; a disconnected installation does not receive instant revocations. Maintain an accurate server clock.
14. LiveKit voice, video and livestreams
Install LiveKit using its official production deployment guide and record the deployed version. Website availability does not prove media connectivity. Use rtc.example.com with valid TLS, and proxy its HTTPS/WebSocket endpoint to loopback port 7880:
location / {
proxy_pass http://127.0.0.1:7880;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}Set these private API environment values:
LIVEKIT_URL=wss://rtc.example.com
LIVEKIT_API_KEY=YOUR_LIVEKIT_KEY
LIVEKIT_API_SECRET=YOUR_LIVEKIT_SECRET
LIVEKIT_HTTP_URL=http://127.0.0.1:7880Configure a signed webhook in LiveKit using the same API key:
webhook:
api_key: YOUR_LIVEKIT_KEY
urls:
- https://app.example.com/v1/livekit/webhookPreserve signature verification. Server events reconcile call duration and livestream participation. For UDP multiplexing configured with rtc.udp_port: 7882, allow 7882/UDP and ICE/TCP 7881/TCP. If using a UDP port range instead, allow that configured range. TURN/UDP, TURN/TLS and relays require their own configured ports, domains and certificates; do not assume opening only 443 is sufficient. Use your real public IP/NAT settings.
sudo systemctl restart mef-api
curl -fsS https://app.example.com/v1/capabilitiesTest between two real devices on Wi-Fi and mobile data. Verify permission denial, reconnect, call termination, duration, livestream chat, gifts and current viewer counts. Capabilities reports configuration; it does not replace a media acceptance test.
15. Message translation
Message translation is separate from interface localization. Install a LibreTranslate-compatible service privately on loopback, install the required language pairs, and set TRANSLATION_URL=http://127.0.0.1:5000 in the API environment. Restart the API. Users select their native language in their profiles; message translation targets that language.
Check the service's actual installed languages. Do not assume every model is available. Test access with real participant accounts and confirm a nonparticipant cannot translate another chat's message. Do not expose an unauthenticated translation service publicly.
16. The 22 interface languages
Web and Android share catalogs for Turkish, English, German, French, Arabic, Chinese, Russian, Spanish, Korean, Japanese, Portuguese, Polish, Italian, Dutch, Ukrainian, Hindi, Indonesian, Vietnamese, Thai, Persian, Bengali and Urdu. Supported device languages are detected automatically; unsupported languages fall back to English. The language menu stores the user's explicit preference. Arabic, Persian and Urdu use right-to-left layout.
Edit interface translations in frontend/src/i18n/locales/<language>.json. Keys are source Turkish strings; values are translations. Preserve placeholders such as {p0}. UI localization must not translate customer names, chat messages, profile biographies or livestream titles. Operating-system permission descriptions live under frontend/locales/; changes require a native rebuild. The documentation itself is available in Turkish and English, selected automatically at /docs, with explicit /docs/tr and /docs/en links.
17. Android configuration and builds
Apply the Android overlay only to the matching Web source version. Back up your .env, branding and application configuration before merging. The combined source already contains Android support. Both clients use the same HTTPS API, accounts, administrator panel and Supabase project.
Edit these fields in frontend/app.json, retaining all other plugins and settings:
{
"expo": {
"name": "My App",
"slug": "my-app",
"scheme": "myapp",
"android": { "package": "com.example.myapp", "versionCode": 1 }
}
}The package name identifies the installed app, not a server domain. Auth redirects read the configured Expo scheme automatically. After changing it, add myapp:// to Supabase Redirect URLs and rebuild. There is no need to edit scheme strings in src/useApp.ts.
Native WebRTC requires a native build; Expo Go does not contain all required modules. An Android entitlement is required for the official Android client and source download.
cd frontend
npm ci
npm run typecheck
npm run lint
npx eas-cli --help
npx eas-cli login
npx eas-cli build:configureExample frontend/eas.json:
{
"build": {
"preview": {
"distribution": "internal",
"environment": "preview",
"android": { "buildType": "apk" }
},
"production": {
"environment": "production",
"android": { "buildType": "app-bundle" }
}
}
}Configure the three EXPO_PUBLIC_ values in both EAS environments. Do not assume a local ignored .env will be uploaded. Never include server secrets in EAS client environment variables.
npx eas-cli env:create --help
npx eas-cli build --platform android --profile preview
npx eas-cli build --platform android --profile productionPreview produces an APK; production produces an AAB for Google Play. Use your own upload/release key, retain it securely, and increment versionCode on updates. A vendor debug/test certificate must not be used for store publication. EAS is a separate service with its own account limits and charges.
For local builds, use SDK 58's generated Android requirements and run npx expo prebuild --platform android. Configure native behavior through app.json and plugins, not manual edits to generated files. A task named assembleRelease does not by itself establish a suitable store signing configuration.
If the API domain changes, update frontend/EAS environment values and rebuild Web and Android. If only the public IP changes while the domain remains the same, DNS may be enough for the client, but TLS, server configuration and private activation state still need a correct migration.
18. Updates and backups
Back up before upgrading. Test on a separate environment, compare migration histories, apply required migrations, use npm ci with the supplied lockfiles, build the API and website, and deploy them together. Keep the previous release available for rollback. Do not overwrite private configuration with example files.
| Backup item | Why it matters |
|---|---|
| Supabase database | Accounts, profiles, chats, wallet and operational records |
| Storage objects | A database backup alone does not guarantee photo/file bytes |
| /etc/mef/api.env | Server secrets and stable bank encryption key |
| /var/lib/mef-license/installation.json | Private installation activation state |
| Android signing/upload key | Ability to update the same application |
| Customized source and brand assets | Your own changes |
Keep backups outside the document root, copy them to a separate secure location, and test restoration. Do not include activation state or private settings in a resale source ZIP. When moving one installation, transfer private state securely rather than cloning it into multiple production installations. The vendor's private license signing key and seller database are not part of customer packages.
19. Troubleshooting
| Symptom | Check |
|---|---|
| 502 Bad Gateway | API service state, journal and loopback port; Nginx proxy target |
| Login succeeds but admin is denied | Verified exact email in operator_emails and canonical admin access |
| Confirmation or recovery fails | SMTP, Site URL, allowed redirects and Android scheme |
| Wrong API after editing .env | Rebuild the web/native client; environment values are embedded |
| License domain verification fails | Public HTTPS challenge reachability, DNS, redirects and CDN challenges |
| License state cannot be saved | mef ownership and mode of /var/lib/mef-license |
| Public key cannot be read | Read and directory traversal permission on its entire path |
| Camera/microphone does not work | HTTPS, permissions, native build and LiveKit ports/NAT |
| Video connects on one network only | UDP/TCP/TURN configuration and provider firewall |
| Translation is unavailable | Loopback translator health and installed language pairs |
| Android API is license-blocked | Android entitlement and refreshed installation lease |
| Bank details cannot be decrypted | Original stable CASHOUT_ENCRYPTION_KEY; restore from secure backup |
Useful diagnostic commands:
sudo systemctl status mef-api --no-pager
sudo journalctl -u mef-api -n 100 --no-pager
sudo nginx -t
curl -fsS http://127.0.0.1:3001/health
curl -fsS https://app.example.com/healthRedact credentials, tokens, bank details and private user content before sharing diagnostics. Do not fix permission errors by disabling RLS, administrator checks, webhook validation or license enforcement.
20. Acceptance checklist and official references
- Confirm registration, email verification, sign-in, recovery and sign-out.
- Complete birth date, native language, language levels and photo onboarding.
- Test two-user matching, likes, message delivery, one-view media and unmatching.
- Test voice/video and livestreams on separate real devices and networks.
- Verify
/admin, operator authorization, licensing and Android entitlement. - Check the 22 language selections, persistence, RTL layouts and custom brand copy.
- Restore a backup in a separate environment and verify HTTPS renewal.
- Configure and test any optional payment, SMS or push provider separately; a blank configuration field does not provide the integration.
Official references:
- Node.js downloads
- Supabase database migrations
- Supabase db push
- Supabase redirect URLs
- Supabase SMTP
- Expo SDK 58
- Expo environment variables
- EAS build configuration
- Android APK builds
- LiveKit production deployment
- LiveKit ports and firewall
Check current official service documentation and release notes before upgrading dependencies or infrastructure.