Skip to content

Creating a Theme

FOSSBilling uses Twig for templating. Themes work by overriding the templates shipped by modules:

  1. Module provides a default template (modules/Example/templates/client/mod_example_index.html.twig)
  2. Your theme provides an override with the same filename (themes/mytheme/html/mod_example_index.html.twig)
  3. FOSSBilling uses your theme's version

FOSSBilling loads templates in this order (highest priority first):

  1. themes/mytheme/html_custom/ — User customizations
  2. themes/mytheme/html/ — Your theme files
  3. modules/Example/templates/client/ or modules/Example/templates/admin/ — Module defaults for the current area
themes/mytheme/
├── assets/ # CSS, JS, images
│ ├── css/
│ ├── js/
│ └── img/
├── config/
│ └── settings.html.twig # Theme settings UI (optional)
├── html/ # Your templates
│ ├── layout_default.html.twig # Base layout
│ ├── layout_public.html.twig # Public pages layout
│ ├── partial_menu.html.twig # Shared partial
│ └── mod_index_dashboard.html.twig # Optional module override
├── html_custom/ # For user customizations (leave empty)
└── manifest.json # Theme metadata
{
"name": "My Theme",
"description": "A custom FOSSBilling theme",
"version": "1.0.0",
"author": "Your Name",
"author_url": "https://example.com",
"icon": "icon.png"
}

Use api_url to build browser API URLs and fb_api_form or fb_api_link to let the JavaScript API wrapper handle submissions.

<form action="{{ 'profile/update'|api_url }}" {{ fb_api_form({ message: 'Profile updated'|trans }) }}>
<input type="text" name="first_name" value="{{ profile.first_name }}">
<button type="submit">{{ 'Save'|trans }}</button>
</form>

For links that trigger API actions:

<a href="{{ 'client/group_delete'|api_url(query: { id: group.id }, role: 'admin') }}"
{{ fb_api_link({
modal: { type: 'confirm', title: 'Are you sure?'|trans },
reload: true
}) }}>
{{ 'Delete'|trans }}
</a>

Session-authenticated client and admin browser API calls require CSRF protection. The JavaScript API wrapper reads the csrf_token cookie and sends the token automatically.

Use role: 'guest' for public actions such as login, signup, and password reset:

<form action="{{ 'client/login'|api_url(role: 'guest') }}" {{ fb_api_form({ redirect: '/'|url }) }}>
<input type="email" name="email" required>
<input type="password" name="password" required>
<button type="submit">{{ 'Login'|trans }}</button>
</form>

The guest, client, and admin Twig globals call FOSSBilling APIs during server-side template rendering. They are not browser HTTP requests and do not use CSRF tokens.

{{ guest.currency_get_pairs }}
{{ guest.system_version }}
{{ client.invoice_get_list({ 'per_page': 10 }) }}
{{ admin.staff_group_get_pairs }}
{{ admin.extension_config_get({ 'ext': 'mod_seo' }) }}

FOSSBilling provides custom filters for themes:

{{ price | format_currency(currency.code) }} {# Format currency #}
{{ 'client/manage' | url(area: 'admin') }} {# Admin panel link #}
{{ 'css/theme.css' | asset_url }} {# Theme asset URL #}
{{ date | timeago }} {# "2 hours ago" #}
{{ content | markdown_to_html }} {# Parse Markdown #}
  • Use htmlspecialchars when outputting user data
  • Validate all inputs
  • Use fb_api_form and fb_api_link for browser API actions
  • Include CSRF tokens manually for raw requests that do not use the wrapper
  • See the security docs for more
  1. Place your theme in /themes/mytheme/
  2. Go to Settings → Themes in the admin panel
  3. Select and activate your theme
  4. Check both client and admin areas
  5. Test on different screen sizes