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 up | What to edit |
|---|---|
| macOS window title & menu bar | macos/Runner/Info.plist → CFBundleName |
| Android launcher | android/app/src/main/AndroidManifest.xml → android:label |
| iOS home screen | ios/Runner/Info.plist → CFBundleDisplayName |
| Windows window title | windows/runner/main.cpp → the window.Create title |
| Windows file properties | windows/runner/Runner.rc → ProductName, FileDescription |
| Windows installer | pubspec.yaml → msix_config → display_name |
| Linux window & header bar | linux/runner/my_application.cc → gtk_window_set_title, gtk_header_bar_set_title |
| Web | web/manifest.json → name, short_name; web/index.html → <title> and apple-mobile-web-app-title |
| Emails | supabase/functions/send-email/_templates/constants.ts → appName |
| Email sender | supabase/.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:
| Field | Where |
|---|---|
| Binary and bundle name | windows/CMakeLists.txt and linux/CMakeLists.txt → BINARY_NAME; macos/Runner/Configs/AppInfo.xcconfig → PRODUCT_NAME |
| Linux application id | linux/CMakeLists.txt → APPLICATION_ID |
| Windows binary metadata | windows/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:
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.
| Source | Feeds |
|---|---|
app_icon.png | iOS (alpha is stripped, remove_alpha_ios: true) |
app_icon_transparent.png | the iOS dark-mode icon variant, and the splash screen |
app_icon_rounded.png | web, Windows and macOS |
android/android_app_icon.png | the Android legacy launcher icon |
android/android_app_icon_background.png | the Android adaptive icon background layer |
android/android_app_icon_foreground.png | the Android adaptive icon foreground layer, and the Android 12+ splash |
Regenerate.
dart run flutter_launcher_iconsThe 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.
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: falseThen regenerate:
dart run flutter_native_splash:createTwo 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.
Logo
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.
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:
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:
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:
[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:
| Object | Used by |
|---|---|
assets/app/logo.svg | the header of every transactional email |
assets/defaults/avatars/user-avatar-1.png | the invitation email, for an invitee with no avatar Multi-tenancy only |
assets/defaults/avatars/org-avatar-1.png | the 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:
supabase stop --no-backup && supabase startA 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.
Legal documents
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.
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:
# 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_URLand leaveTERMS_OF_USE_URLempty 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
httporhttpsURI. Anything else, a typo, amailto:, a baremyapp.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.envtoo, or your store build will show the placeholder documents. See Building for release.
Where the links appear
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.