Skip to content

Apps and clients

New client instance

Every client gets their own instance: their own backend, database, keys and accounts, and their own app, published from their own store accounts. This page is the repeatable path to stand one up.

Tenancy model

Sharkhex Delivery is single-tenant: one deployment serves one client. There is no tenant or organisation layer in the data; everything in a database belongs to that one platform.

  • Isolation: customers, orders, payments and payouts of different businesses never share a database, and each instance has its own secrets, JWT keys and storage.
  • Own accounts: Stripe, the email provider, Pusher, Google Maps, Expo and the app stores are the client's own accounts, configured in that instance's settings. App stores require white-label apps to be published from the client's own developer accounts.
  • Simple updates: an instance is updated by deploying a newer release and running its migrations; instances never affect each other.

What is per instance: the backend .env.local (URL, database, secrets, brand name), the JWT key pair, the database (settings, legal documents, users, orders), uploaded files and private documents, and the app's brand manifest. What is shared: only the codebase release.

1. Accounts the client owns

  • A domain with HTTPS, and a server or hosting plan meeting the backend requirements.
  • Stripe (payments, and Connect if couriers or stores are paid through Stripe).
  • Mailgun (transactional email), Pusher (realtime), Google Maps API keys.
  • Expo account and an EAS project; Apple Developer (organisation) and Google Play Console accounts.
  • Optional: an S3 bucket for uploads and private documents.
  • Optional: Sentry (crash and error reports for the app and the backend) and Firebase (usage analytics, collected only with the user's consent).

2. Backend

With Docker, follow Docker deployment for steps 1 to 4 and 8, then continue at step 5. Without Docker:

  1. Check out the release and install dependencies:

    composer install --no-dev --optimize-autoloader
  2. Prepare the instance. This writes .env.local with a new APP_SECRET and JWT passphrase, production settings, the public URL (used for links in emails and cron jobs), the database URL and the brand name, then generates the JWT key pair. It refuses to run on a checkout that is already an instance, so it can never rotate a live instance's keys.

    php bin/new-instance --url=https://delivery.example.com \
      --database-url="mysql://user:password@127.0.0.1:3306/delivery?serverVersion=8.0&charset=utf8mb4" \
      --brand="Example Delivery" --from-email=noreply@example.com
  3. Create the database tables:

    php bin/console doctrine:database:create --if-not-exists
    php bin/console app:database:install
  4. Point the web server's document root at public/ and open https://your-domain/install to create the admin account and enter the platform details.

  5. In the admin, fill in Settings: General (legal details, public URL, currency, timezone), Payments (Stripe keys; webhook /api/v1/webhook/stripe), Notifications (Mailgun), Storage, Integrations (Pusher, Maps), Commission, Tax, Delivery and Courier onboarding.

  6. Review and publish the legal documents (Legal documents → Create drafts from templates).

  7. Draw the delivery zones (Locations → Delivery zones). Until the first zone is active, the platform delivers everywhere: see Platform configuration.

  8. For push notifications, put the client's Expo access token in EXPO_DSN in .env.local: expo://ACCESS_TOKEN@default.

  9. Add the scheduled jobs to cron.

  10. Optional: error reports. Put the DSN of the client's Sentry project for the backend in SENTRY_DSN in .env.local. Releases are named after the VERSION file; request bodies, cookies and IP addresses are never sent, and client errors such as 404s are left out.

  11. Point an uptime monitor at https://your-domain/api/v1/health (public, answers 200).

3. App

  1. Create the client's brand from their logo (the main colour is suggested from it, or pass --primary):

    npm run brand:new -- example-delivery --name "Example Delivery" \
      --bundle-id com.example.delivery --api-url https://delivery.example.com/api \
      --support-email help@example.com --logo ~/Downloads/example-logo.png
  2. Check the suggested colour (npm run brand:suggest -- example-delivery) and the generated icons in generated/brand/assets/ after applying; create the EAS project and put its id in identity.easProjectId.

  3. Pick the typeface (typography.family, or the client's own files) and the languages the app offers (locales.enabled and locales.default); see Typeface and languages in Mobile app setup.

  4. Set the build environment: EXPO_PUBLIC_PUSHER_KEY, EXPO_PUBLIC_PUSHER_CLUSTER, EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY, EXPO_PUBLIC_GOOGLE_MAPS_API_KEY, EXPO_PUBLIC_GOOGLE_MAPS_API_KEY_ANDROID.

  5. Optional: add the client's Sentry project and Firebase config files to the brand (see Crash reports and Usage analytics in Mobile app setup).

  6. Apply and check the brand, then build: npm run brand:apply -- example-delivery, npm run brand:check, then build with EAS (see Building and publishing).

Currency, country, default language, support email and the legal pages come from the backend at runtime, so they are set in the instance's admin, not in the app.

4. Store submission

Follow the store section of the Deployment checklist: privacy policy URL https://your-domain/legal/privacy, account deletion URL https://your-domain/account/delete.

Create the reviewers' demo accounts (a customer, an approved courier, a live store with products) and add their emails to Settings → Sign-in security → App review accounts. With the portal connected (Build your apps), its launch readiness checks confirm all of this before submission.

5. Updating an instance

  1. Deploy the newer release, run composer install --no-dev --optimize-autoloader, then php bin/console doctrine:migrations:migrate (or use Admin → Platform Updates).
  2. For the app, apply the same brand on the new release and rebuild. A fix that only changes JavaScript can go out as an over-the-air update instead (see Over-the-air updates).

Found a mistake? Tell us.