Every letting agency has unique integration needs. One agency needs Rightmove and Zoopla for advertising. Another requires accounting system integration. A third wants SMS notifications through Twilio. Building each integration as custom code doesn't scale—you quickly accumulate technical debt and maintenance burden.
Week 37 introduced an extensible apps system that treats integrations as data rather than code. Adding a new integration becomes straightforward: define configuration requirements in JSON, implement the service logic, and the system automatically generates installation UI, handles settings encryption, and manages multi-tenant installations. This article explores the architecture that makes this flexibility possible.
What Your Team Will Notice
The apps interface resembles modern app marketplaces like Shopify or Salesforce. Available integrations appear as cards with logos and descriptions. Click "Install" on Rightmove, fill in your branch details and certificate information, and property data starts flowing to Rightmove automatically.
Each installed app appears in the integrations dashboard with status indicators showing whether it's active and working correctly. Clicking an app reveals settings you can update—change your Rightmove branch ID, update webhook URLs, or modify which property types sync automatically. Toggle switches activate or deactivate apps without deleting their configuration, useful when temporarily pausing integrations.
For agencies managing multiple branches, the apps system respects multi-tenancy. Each agency maintains its own app installations with separate configuration. One agency's Rightmove credentials never mix with another's, and deactivating an app affects only that agency's data flows.
Under the Bonnet: JSON-Driven Configuration
The apps system's flexibility stems from treating app definitions as structured data rather than hardcoded logic. All available apps are defined in a single configuration file:
{
"apps": [
{
"name": "Rightmove",
"slug": "rightmove",
"description": "Automatically advertise properties on Rightmove",
"category": "property_portals",
"image_url": "/assets/app-logos/rightmove.png",
"active": true,
"configuration_schema": {
"type": "object",
"properties": {
"branch_id": {
"type": "string",
"title": "Branch ID",
"description": "Your Rightmove branch identifier",
"required": true
},
"certificate": {
"type": "string",
"title": "P12 Certificate",
"description": "Upload your Rightmove API certificate",
"format": "textarea",
"required": true
}
}
}
}
]
}
This JSON definition captures everything needed to create an installable app: display information (name, description, logo), categorisation for grouping related apps, and most critically, a configuration schema defining what settings users must provide.
Dynamic Form Generation with JSON Schema
The configuration schema uses JSON Schema, an industry-standard format for describing data structures. The apps system reads this schema and automatically generates appropriate form fields:
# App model (simplified)
class App
field :name
field :slug
field :description
field :category
field :configuration_schema # JSONB column storing schema
field :active
def parsed_configuration_schema
# Parses JSON schema and extracts field definitions
# Identifies required vs optional fields
# Determines input types (text, textarea, select, checkbox)
end
def required_fields
# Returns list of required configuration keys
# Used for validation before app activation
end
end
When displaying the installation form, the view iterates through schema properties and renders appropriate inputs:
<!-- Dynamic form generation -->
<% @app.parsed_configuration_schema['properties'].each do |field_name, field_schema| %>
<div class="form-field">
<label><%= field_schema['title'] %></label>
<% if field_schema['format'] == 'textarea' %>
<textarea name="settings[<%= field_name %>]"
placeholder="<%= field_schema['description'] %>"
<%= 'required' if field_schema['required'] %>></textarea>
<% else %>
<input type="text"
name="settings[<%= field_name %>]"
placeholder="<%= field_schema['description'] %>"
<%= 'required' if field_schema['required'] %> />
<% end %>
</div>
<% end %>
This approach means adding a new configuration field requires only editing JSON—no view changes, no controller modifications, no migrations.
Encrypted Settings Storage
App configuration often includes sensitive credentials like API keys, certificates, and secrets. The AppInstallation model stores these securely using Rails encrypted attributes:
class AppInstallation
belongs_to :app
belongs_to :agency
# Encrypted storage for sensitive configuration
encrypts :settings
field :active # Can be toggled without losing settings
field :settings # JSONB encrypted at rest
def valid_configuration?
# Validates all required fields are present
# Checks field format matches schema expectations
end
end
Encryption happens automatically before database writes and decrypts transparently when reading settings. This protects credentials even if someone gains database access, following security best practices from services like AWS Secrets Manager and HashiCorp Vault.
Automated App Synchronisation
App definitions live in config/apps.json, making them version-controlled and reviewable like code. A rake task synchronises these definitions to the database:
# lib/tasks/apps.rake
namespace :apps do
desc "Sync apps from JSON to database"
task sync: :environment do
# Reads config/apps.json
# For each app definition:
# - Creates app if it doesn't exist (based on slug)
# - Updates existing app if definition changed
# - Preserves existing app installations during updates
# - Reports created/updated/unchanged counts
end
end
This task runs automatically during Heroku deployment as part of the release phase:
# Procfile
release: bundle exec rails apps:sync && bundle exec rails db:migrate
When you deploy code containing a new app definition, it becomes immediately available for installation without manual database manipulation. Updating an existing app's description or configuration schema updates all instances whilst preserving agency installations and their settings.
Professional UI with Minimal Code
The apps interface uses TailAdmin's card components for consistent visual design:
<!-- Available apps grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<% @available_apps.each do |app| %>
<div class="card">
<div class="card-body">
<!-- App logo in top-right -->
<% if app.image_url.present? %>
<%= image_tag app.image_url, class: "app-logo" %>
<% else %>
<!-- Fallback to initials -->
<div class="app-logo-fallback"><%= app.name[0..1].upcase %></div>
<% end %>
<h3><%= app.name %></h3>
<p class="text-muted"><%= app.description %></p>
<%= link_to "Install", new_app_installation_path(app_id: app.id),
class: "btn btn-primary" %>
</div>
</div>
<% end %>
</div>
Installed apps display with toggle switches powered by Alpine.js and Turbo Streams:
<!-- Installed app card with toggle -->
<div class="card" data-controller="app-toggle">
<div class="card-body">
<h3><%= installation.app.name %></h3>
<!-- Status badge -->
<span class="badge <%= installation.active? ? 'badge-success' : 'badge-muted' %>">
<%= installation.active? ? 'Active' : 'Inactive' %>
</span>
<!-- Toggle switch -->
<label class="toggle">
<input type="checkbox"
<%= 'checked' if installation.active? %>
data-action="change->app-toggle#update"
data-url="<%= app_installation_path(installation) %>">
<span class="toggle-slider"></span>
</label>
<!-- Settings link -->
<%= link_to "Settings", edit_app_installation_path(installation),
class: "btn btn-outline" %>
</div>
</div>
The toggle updates via Turbo Stream without full page reload, providing instant visual feedback whilst updating the database asynchronously.
Multi-Tenant Isolation
Every app installation belongs to a specific agency, enforced through acts_as_tenant:
class AppInstallation
belongs_to :agency
acts_as_tenant :agency
# Scope all queries by current agency
# Prevents cross-tenant data access
end
When Agency A installs Rightmove, that installation record includes agency_id: A. When Agency B later installs Rightmove, it creates a separate installation with agency_id: B. Each agency's settings remain isolated, and querying installations automatically filters by current tenant context.
This isolation extends to app usage. When Rightmove integration needs credentials, it retrieves the installation for the current agency only:
class RightmoveApiService
def initialize(property)
@property = property
@installation = AppInstallation.find_by!(app: App.find_by!(slug: 'rightmove'))
@settings = @installation.settings
end
end
The acts_as_tenant filter ensures find_by! only searches within the current agency's installations, preventing accidental credential mixing.
Testing Dynamic Configuration
Testing an apps system requires verifying both static behaviour (app creation, installation management) and dynamic behaviour (schema-driven form generation):
RSpec.describe AppInstallation do
it "validates required fields from configuration schema" do
# Verifies validation logic extracts required fields correctly
end
it "encrypts settings before database storage" do
# Confirms encryption happens transparently
end
it "scopes installations by agency with acts_as_tenant" do
# Ensures multi-tenant isolation works correctly
end
end
RSpec.describe "Dynamic form generation" do
it "renders text inputs for string fields" do
# Verifies form generation from schema
end
it "renders textareas for fields with textarea format" do
# Confirms format handling works
end
it "marks required fields appropriately" do
# Tests required field rendering
end
end
Integration tests verify the complete flow: installing an app, configuring settings, activating it, updating configuration, and deactivating without losing settings.
What's Next
With the apps architecture established, Week 37 immediately put it to the test by implementing Rightmove integration—a sophisticated enterprise API requiring certificate authentication, complex payload structures, and photo handling. The apps system proved flexible enough to handle this complexity whilst maintaining simple configuration for less demanding integrations like webhook notifications.
Future enhancements might include app marketplace features (user reviews, installation counts, featured apps), app permissions (granular control over what data each app can access), and app developer tools (allowing third parties to build and publish apps following our schema standards).
The apps system transforms LetAdmin from a monolithic platform into an extensible ecosystem, enabling rapid integration development whilst maintaining security, multi-tenancy, and operational simplicity.
