MEF · Installation documentation

Web + Android · Ubuntu · Supabase

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.

AddressPurpose
https://app.example.comApplication website and API in this guide
https://app.example.com/adminApplication administration
wss://rtc.example.comLiveKit voice, video and livestream signaling
https://PROJECT_REF.supabase.coYour Supabase Auth, database and storage
https://license.voxexo.comVendor 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

PackageContents
MEF-Web-Source.zipWeb frontend, API, SQL migrations, deployment templates and documentation
MEF-Android-Addon.zipNative source overlay requiring the matching Web source version and Android entitlement
MEF-Web-Android-Source.zipCombined Web and Android sources
APKInstallable Android test/build output; not editable source
AABGoogle 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

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 dashboardFieldValue or example
frontend/.envEXPO_PUBLIC_API_URLhttps://app.example.com — no /v1 suffix
frontend/.envEXPO_PUBLIC_SUPABASE_URLhttps://PROJECT_REF.supabase.co
frontend/.envEXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEYYour project's public publishable/anon key
/etc/mef/api.envSUPABASE_URL, SUPABASE_ANON_KEYSame project URL and public key
/etc/mef/api.envSUPABASE_SERVICE_ROLE_KEYPrivate server-only service_role key
/etc/mef/api.envCORS_ORIGINShttps://app.example.com; exact origins separated by commas
/etc/mef/api.envLICENSE_INSTALLATION_ORIGINhttps://app.example.com; no trailing slash
/etc/mef/api.envLICENSE_ISSUERhttps://license.voxexo.com
/etc/mef/api.envLICENSE_PUBLIC_KEY_FILE/opt/mef/server/license-public.pem
/etc/mef/api.envLICENSE_STATE_FILE/var/lib/mef-license/installation.json
frontend/app.jsonexpo.name, slug, schemeMy App, my-app, myapp
frontend/app.jsonexpo.android.package, versionCodecom.example.myapp, 1; increment for store updates
frontend/tools/finalize-web.mjstitle, description and URLYour branding and website origin
/etc/nginx/sites-available/mefserver_name, root, proxy_passapp.example.com, /var/www/mef, http://127.0.0.1:3001
Supabase Auth → URL ConfigurationSite URL, Redirect URLshttps://app.example.com/ and myapp://
Supabase Auth → Email → SMTPServer, port, user, password, senderYour email provider's settings
/etc/mef/api.envLIVEKIT_URL, LIVEKIT_HTTP_URLwss://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.

bash
getent ahosts app.example.com
getent ahosts rtc.example.com

Use 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:

bash
sudo apt update
sudo apt install -y ca-certificates curl unzip python3 openssl nginx certbot python3-certbot-nginx

Install 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.

bash
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-license

Skip 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:

bash
ls /opt/mef/frontend/package.json /opt/mef/server/package.json
sudo chown -R mef:mef /opt/mef/frontend /opt/mef/server

Keep 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:

bash
cd /opt/mef
npx supabase --version
npx supabase init --help
npx supabase link --help
npx supabase db push --help

Run npx supabase init only when local configuration is missing. Then:

bash
npx supabase login
npx supabase link --project-ref PROJECT_REF
npx supabase db push --dry-run
npx supabase db push
npx supabase migration list

Use 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:

sql
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:

sql
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

bash
sudo install -o root -g root -m 600 /opt/mef/server/.env.example /etc/mef/api.env
sudo nano /etc/mef/api.env

Fill in the fields below. Preserve other template fields that your installation needs:

dotenv
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=true

Generate 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:

bash
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-license

Keep 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

bash
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 build

Create /etc/systemd/system/mef-api.service:

ini
[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.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now mef-api
sudo systemctl status mef-api --no-pager
curl -fsS http://127.0.0.1:3001/health

A 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

bash
cd /opt/mef/frontend
sudo -u mef cp .env.example .env
sudo -u mef nano .env
dotenv
EXPO_PUBLIC_SUPABASE_URL=https://PROJECT_REF.supabase.co
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_PROJECT_PUBLIC_KEY
EXPO_PUBLIC_API_URL=https://app.example.com

Do 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.

bash
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:

nginx
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; }
}
bash
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/health

Verify 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:

bash
sudo python3 /opt/mef/documentation/activate-owner.py /opt/mef/server /etc/mef/api.env

Enter 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:

nginx
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:

dotenv
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:7880

Configure a signed webhook in LiveKit using the same API key:

yaml
webhook:
  api_key: YOUR_LIVEKIT_KEY
  urls:
    - https://app.example.com/v1/livekit/webhook

Preserve 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.

bash
sudo systemctl restart mef-api
curl -fsS https://app.example.com/v1/capabilities

Test 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:

json
{
  "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.

bash
cd frontend
npm ci
npm run typecheck
npm run lint
npx eas-cli --help
npx eas-cli login
npx eas-cli build:configure

Example frontend/eas.json:

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.

bash
npx eas-cli env:create --help
npx eas-cli build --platform android --profile preview
npx eas-cli build --platform android --profile production

Preview 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 itemWhy it matters
Supabase databaseAccounts, profiles, chats, wallet and operational records
Storage objectsA database backup alone does not guarantee photo/file bytes
/etc/mef/api.envServer secrets and stable bank encryption key
/var/lib/mef-license/installation.jsonPrivate installation activation state
Android signing/upload keyAbility to update the same application
Customized source and brand assetsYour 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

SymptomCheck
502 Bad GatewayAPI service state, journal and loopback port; Nginx proxy target
Login succeeds but admin is deniedVerified exact email in operator_emails and canonical admin access
Confirmation or recovery failsSMTP, Site URL, allowed redirects and Android scheme
Wrong API after editing .envRebuild the web/native client; environment values are embedded
License domain verification failsPublic HTTPS challenge reachability, DNS, redirects and CDN challenges
License state cannot be savedmef ownership and mode of /var/lib/mef-license
Public key cannot be readRead and directory traversal permission on its entire path
Camera/microphone does not workHTTPS, permissions, native build and LiveKit ports/NAT
Video connects on one network onlyUDP/TCP/TURN configuration and provider firewall
Translation is unavailableLoopback translator health and installed language pairs
Android API is license-blockedAndroid entitlement and refreshed installation lease
Bank details cannot be decryptedOriginal stable CASHOUT_ENCRYPTION_KEY; restore from secure backup

Useful diagnostic commands:

bash
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/health

Redact 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

Official references:

Check current official service documentation and release notes before upgrading dependencies or infrastructure.