🚀 Join the waitlist now! waitlist.floot.dev
LogoFlootdocs

Customization

Make a generated project yours: app name, icons, splash screen, logo and legal documents.

floot create my_app --org com.acme already wrote your name into more places than you would guess: the Android package, the iOS bundle identifier, the deep-link scheme, the window titles, the "from" address on transactional emails. What it could not invent is everything with a picture or a lawyer attached, the icon, the splash screen, the logo and the two legal documents are still placeholders, and this page is about replacing them.

Colors and fonts live next door

Everything about how the app looks, the color tokens, the seeded ColorScheme, the font families, the component themes, is on the Theming page. This page is about the assets and identity around it. The two overlap in exactly one place, the splash screen, which takes its images from here and its colors from there.

The app name

Two forms of the name are in circulation: the Dart-style package name you typed (my_app) and the human-facing display name (My App). floot create writes the display name into every platform's own idea of an application name at generation time — it defaults to the title-cased project name, and --display-name "My App" overrides it:

floot create my_app --display-name "My App"

There is no floot rename, so changing it after generation means editing the files below by hand. Everything in the first group is already correct in a freshly generated project; the table is what to edit if you change your mind.

Where it shows upWhat to edit
macOS window title & menu barmacos/Runner/Info.plist → CFBundleName
Android launcherandroid/app/src/main/AndroidManifest.xml → android:label
iOS home screenios/Runner/Info.plist → CFBundleDisplayName
Windows window titlewindows/runner/main.cpp → the window.Create title
Windows file propertieswindows/runner/Runner.rc → ProductName, FileDescription
Windows installerpubspec.yaml → msix_config → display_name
Linux window & header barlinux/runner/my_application.cc → gtk_window_set_title, gtk_header_bar_set_title
Webweb/manifest.json → name, short_name; web/index.html → <title> and apple-mobile-web-app-title
Emailssupabase/functions/send-email/_templates/constants.ts → appName
Email sendersupabase/.env.local → SMTP_FROM

macOS: CFBundleName, not PRODUCT_NAME

macos/Runner/Configs/AppInfo.xcconfig says PRODUCT_NAME is "also the title of the Flutter window". It is not — that comment predates the current runner. PRODUCT_NAME names the built binary and the .app bundle (my_app.app); CFBundleName in macos/Runner/Info.plist is what the window title and the menu bar read. Verified by building with the two set to different values.

The rest of the name-shaped fields identify the build, not the app, and stay derived from the package name — floot create deliberately leaves them alone:

FieldWhere
Binary and bundle namewindows/CMakeLists.txt and linux/CMakeLists.txt → BINARY_NAME; macos/Runner/Configs/AppInfo.xcconfig → PRODUCT_NAME
Linux application idlinux/CMakeLists.txt → APPLICATION_ID
Windows binary metadatawindows/runner/Runner.rc → InternalName, OriginalFilename

The support address is not covered by any of this

--org com.acme gives you support@acme.com and noreply@acme.com, which are plausible enough to slip through review and wrong unless you actually own that domain. Open both regardless of what you did with the name:

supabase/functions/send-email/_templates/constants.ts
export const appName = 'My App';
export const supportEmail = 'support@myapp.com';

Leave the Dart package name alone

The name: field in pubspec.yaml is the Dart package name, not a display name; it is the package:my_app/... prefix on every import in lib/ and test/. Renaming it is a project-wide find-and-replace with nothing to gain; no user ever sees it.

App icon

This one is already done, you are re-running it

floot create runs flutter_launcher_icons for you, right after pub get, which is why a freshly generated project already has a real launcher icon on every platform instead of Flutter's default. Replacing the source images does nothing on its own: the build reads the generated platform files, so the generator has to run again.

Design the icons. Both stores have opinions, and they differ:

Replace the sources, keeping the filenames. Six images feed every platform. Each one is pointed at from the flutter_launcher_icons block in pubspec.yaml, so a different filename means editing that block too.

android_app_icon.png
android_app_icon_background.png
android_app_icon_foreground.png
app_icon.png
app_icon_rounded.png
app_icon_transparent.png
SourceFeeds
app_icon.pngiOS (alpha is stripped, remove_alpha_ios: true)
app_icon_transparent.pngthe iOS dark-mode icon variant, and the splash screen
app_icon_rounded.pngweb, Windows and macOS
android/android_app_icon.pngthe Android legacy launcher icon
android/android_app_icon_background.pngthe Android adaptive icon background layer
android/android_app_icon_foreground.pngthe Android adaptive icon foreground layer, and the Android 12+ splash

Regenerate.

Terminal
dart run flutter_launcher_icons

The generator overwrites tracked files, not build artifacts, and none of them are git-ignored, so read the diff before committing it.

Linux is the one platform flutter_launcher_icons does not cover; a Linux desktop icon is a .desktop entry you ship yourself. Everything else is configured under flutter_launcher_icons in pubspec.yaml; see the package documentation for the options not used here.

Splash screen

Same story: floot create already ran flutter_native_splash:create, so the native splash exists. It reuses the icon sources rather than introducing new ones: assets/icons/app_icon_transparent.png everywhere, and assets/icons/android/android_app_icon_foreground.png for the Android 12+ splash API, which insists on a foreground layer it can mask into a circle.

pubspec.yaml
flutter_native_splash:
  color: '#ffffff'
  color_android: '#ffffff'
  color_dark_android: '#111111'
  color_ios: '#ffffff'
  color_dark_ios: '#111111'
  image: 'assets/icons/app_icon_transparent.png'
  android_12:
    color: '#ffffff'
    icon_background_color: '#ffffff'
    image: 'assets/icons/android/android_app_icon_foreground.png'
    color_dark: '#111111'
    icon_background_color_dark: '#111111'
    image_dark: 'assets/icons/android/android_app_icon_foreground.png'
  web: false

Then regenerate:

Terminal
dart run flutter_native_splash:create

Two things this block gets wrong for you

web: false: there is no web splash. If you want one, flip it and regenerate; nothing else in the project depends on it being off.

Those hard-coded hex values are the light and dark background colors, written out by hand rather than read from the theme. If you retint the app, they will not follow; Theming explains which token #111111 is standing in for.

Every other option, branding images, fullscreen mode, per-platform overrides, is documented by the package.

The logo exists twice, because two different runtimes need it and neither can reach the other's copy. The Flutter app loads it from its bundled assets; the transactional email templates cannot bundle anything, so they reference a publicly readable URL served by Supabase Storage.

logo.svg
logo.svg

Replace both. If you keep the filename and the .svg extension, that is the whole job, nothing else needs editing.

If you change the filename or format

Two constants name the file, one per runtime.

The Flutter side, where every widget that draws the logo goes through AppAssets:

lib/core/presentation/utils/constants/assets.dart
final class AppAssets {
  const AppAssets._();

  /// The logo of the app.
  static const String logo = 'assets/app/logo.png'; // was logo.svg
}

The Supabase side, where the URL is assembled from SB_STORAGE_URL and the object's path inside the public assets bucket:

supabase/functions/_shared/assets.ts
export abstract class Assets {
  /** App logo */
  static readonly LOGO = `${env.SB_STORAGE_URL}/public/assets/app/logo.png`;
}

SUPA_STORAGE_URL is now SB_STORAGE_URL

The Supabase-side variables were renamed to the SB_ prefix: SB_URL, SB_PUBLISHABLE_KEY, SB_SECRET_KEY, SB_STORAGE_URL, SB_AUTH_EXTERNAL_REDIRECT_URI, SB_SEND_EMAIL_HOOK_SECRET. If you are carrying an older supabase/.env.local forward, rename them: the Edge Functions validate their environment at startup and refuse to boot on a missing one. Note this is the Supabase env file only, the Flutter app's .env.local still uses SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY.

Storage seeds

supabase/storage_seeds/assets/ is the local contents of the public assets bucket. supabase/config.toml points the bucket at it:

supabase/config.toml
[storage.buckets.assets]
public = true
file_size_limit = "2MiB"
allowed_mime_types = ["image/png", "image/jpeg", "image/svg+xml"]
objects_path = "./storage_seeds/assets"

Three objects ship in it:

ObjectUsed by
assets/app/logo.svgthe header of every transactional email
assets/defaults/avatars/user-avatar-1.pngthe invitation email, for an invitee with no avatar Multi-tenancy only
assets/defaults/avatars/org-avatar-1.pngthe invitation email, for an organization with no icon Multi-tenancy only

Two constraints in that config bite quietly. allowed_mime_types rejects anything that is not PNG, JPEG or SVG (a WebP logo is refused at seed time, not at render time), and file_size_limit caps each object at 2 MiB.

If a replaced seed does not show up locally

Seeding happens when Supabase provisions the bucket, not on every start. The reliable way to force it is to discard the volume:

Terminal
supabase stop --no-backup && supabase start

A plain supabase stop && supabase start restores the saved volume and skips seeding, so the old file survives.

Nothing seeds a hosted project

objects_path is a local-development convenience. On a hosted Supabase project you create the public assets bucket and upload the same paths yourself, Studio → Storage, or the CLI, and point SB_STORAGE_URL at that project's storage endpoint. Miss this and your emails render with broken images, which is the kind of thing nobody notices until a customer mentions it.

A new project ships a privacy policy and terms of use written for a fictional company.

Important

DO NOT ship these documents as-is. They are placeholders, not legal advice, and not binding. Floot is not responsible for the consequences of shipping them unchanged.

You have two ways to present them, and the choice changes the user experience more than you would expect.

Option 1: bundled Markdown (the default)

Two Markdown files, loaded from the app bundle and rendered in a dialog.

privacy_policy.md
terms_of_use.md

Replace the contents of assets/legal/terms_of_use.md and assets/legal/privacy_policy.md with your own documents, in Markdown.

If you rename either file, update AppAssets.termsOfUse and AppAssets.privacyPolicy in lib/core/presentation/utils/constants/assets.dart to match. The whole assets/legal/ directory is already declared in pubspec.yaml, so a new file in it is bundled without further edits.

This is the mode that gives you a consent moment. When the user taps Terms of use or Privacy policy under the sign-up form, on the paywall or in the feedback composer, the document opens in a dialog that is deliberately not dismissible by tapping outside it, and whose only action is labelled Confirm. Reaching the app again requires pressing that button.

That is the whole of the mechanism, so be clear-eyed about it: the dialog is a deliberate friction point, not a recorded agreement. Nothing is written down, and sign-up is not blocked on having opened it. The Account → About entries use the same documents through an ordinary dismissible dialog, because they are reference material rather than consent.

Option 2: hosted URLs

Set either variable in the app's .env.local and that document is fetched from your website instead of the bundle:

.env.local
# The URL of the hosted terms of use document.
TERMS_OF_USE_URL="https://myapp.com/terms"
# The URL of the hosted privacy policy document.
PRIVACY_POLICY_URL="https://myapp.com/privacy"

The upside is real: legal text you can correct without shipping a build.

Setting a URL makes consent non-blocking

The two modes are not visually different versions of the same thing. With a URL configured, the document opens in an in-app browser view, and the dialog's two consent properties, the non-dismissible barrier and the Confirm action, are ignored, because a browser view has neither. The user swipes it away like any other web page.

So the flow changes from "acknowledge this document to continue" to "here is a link". If your compliance story depends on an explicit acknowledgement at sign-up, keep the bundled documents, or re-add the gate yourself.

Three more behaviours worth knowing before you set these:

  • The choice is per document. Set PRIVACY_POLICY_URL and leave TERMS_OF_USE_URL empty and you get exactly that: a browser view for the privacy policy, the consent dialog for the terms. An empty value means "bundled", and that is the shipped default for both.
  • A bad URL fails loudly, it does not fall back. The value must parse as an http or https URI. Anything else, a typo, a mailto:, a bare myapp.com/terms, shows an error toast and opens nothing. It never quietly reverts to the bundled document.
  • Release builds read .env, not .env.local. If you set these for production, set them in .env too, or your store build will show the placeholder documents. See Building for release.

Four places, so one edit is visible in all of them: under the sign-up form and on the paywall (both documents), in the feedback composer (privacy policy only), and in Account → About (both, as permanent entries).

Have a lawyer look at the final text. A privacy policy that does not match what your app actually collects is worse than none, and both stores reject submissions over it.

What's next?

On this page