Letting agencies manage hundreds of property keys simultaneously—front doors, back doors, communal entrances, meter cupboards, outbuildings. These physical assets enable viewings, maintenance access, and emergency entry, yet many agencies track them using whiteboards, spreadsheets, or verbal handoffs. Keys disappear, staff forget who borrowed which set, and locating the spare key for an urgent viewing becomes a frustrating treasure hunt.
Week 45 implemented comprehensive property key management tracking physical keys digitally throughout their lifecycle. The system records key creation with identification details, handles checkout to staff members with expected return dates, processes returns updating availability automatically, and broadcasts real-time status changes so multiple staff see current key locations instantly. This article explores the key management data model, checkout workflow implementation, real-time updates via ActionCable, and reminder automation for overdue keys.
What Your Team Will Notice
Each property now displays a "Keys" section showing all associated keys: "Front Door - Yale", "Back Door - Chubb", "Meter Cupboard". Each key shows current status—Available (green indicator), Checked Out (amber indicator with holder name), or Overdue (red indicator with days overdue).
Checking out keys requires just two actions: click "Check Out" beside the desired key, select staff member and expected return date from the modal dialogue. The system records checkout timestamp, sends confirmation email to the person collecting keys, and updates the interface instantly showing key as unavailable. Other staff viewing the same property see the status change immediately without page refresh—real-time synchronisation prevents double-bookings where two people believe keys are available.
Returning keys is equally simple: click "Return" beside the checked-out key, optionally add notes about key condition or issues encountered, and the system timestamps the return making keys available again. The checkout history preserves complete audit trail: every checkout and return logged with timestamps, user identities, and purposes.
Overdue keys trigger automatic reminders: if a key remains checked out past expected return date, the system emails the holder daily reminder ("You checked out keys for 123 High Street on Monday, expected return was yesterday"). These gentle nudges reduce the common scenario where staff genuinely forget they still have keys in desk drawers or car glove compartments.
The keys interface provides at-a-glance visibility: properties with available keys show green indicators, properties with all keys checked out show amber, and properties with overdue keys show red. This visual system lets office managers identify problems quickly during morning briefings without interrogating individual records.
Under the Bonnet: Key Management Data Model
The data model balances flexibility with practical constraints:
# db/migrate/...create_property_keys.rb
class CreatePropertyKeys < ActiveRecord::Migration[7.0]
def change
create_table :property_keys do |t|
t.references :property, null: false, foreign_key: true
t.string :key_type, null: false
t.string :key_label
t.text :description
t.string :location
t.integer :quantity, default: 1
t.string :status, default: 'available', null: false
t.timestamps
end
add_index :property_keys, [:property_id, :key_type]
end
end
# db/migrate/...create_key_checkouts.rb
class CreateKeyCheckouts < ActiveRecord::Migration[7.0]
def change
create_table :key_checkouts do |t|
t.references :property_key, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.references :checked_out_by, null: false, foreign_key: { to_table: :users }
t.datetime :checked_out_at, null: false
t.datetime :expected_return_at
t.datetime :returned_at
t.references :returned_by, foreign_key: { to_table: :users }
t.text :checkout_notes
t.text :return_notes
t.string :checkout_reason
t.timestamps
end
add_index :key_checkouts, [:property_key_id, :returned_at]
add_index :key_checkouts, :expected_return_at
end
end
This structure separates key inventory (property_keys) from checkout transactions (key_checkouts), enabling complete history tracking whilst maintaining current availability status efficiently.
Property Key Model
The model handles business logic and validations:
# app/models/property_key.rb
class PropertyKey < ApplicationRecord
belongs_to :property
has_many :key_checkouts, dependent: :destroy
has_one :current_checkout, -> { where(returned_at: nil) }, class_name: 'KeyCheckout'
validates :key_type, presence: true
validates :quantity, numericality: { greater_than: 0 }
validates :status, inclusion: { in: %w[available checked_out unavailable] }
after_update_commit :broadcast_status_change
scope :available, -> { where(status: 'available') }
scope :checked_out, -> { where(status: 'checked_out') }
scope :overdue, -> {
joins(:current_checkout)
.where('key_checkouts.expected_return_at < ?', Time.current)
.where('key_checkouts.returned_at IS NULL')
}
def available?
status == 'available' && current_checkout.nil?
end
def checked_out?
status == 'checked_out' && current_checkout.present?
end
def overdue?
return false unless checked_out?
current_checkout.expected_return_at < Time.current
end
def display_name
key_label.presence || "#{key_type.titleize} Key"
end
private
def broadcast_status_change
ActionCable.server.broadcast(
"property_#{property_id}_keys",
{
event: 'key_status_changed',
key_id: id,
status: status,
current_holder: current_checkout&.user&.name,
expected_return: current_checkout&.expected_return_at
}
)
end
end
This model provides convenience methods checking availability status, scopes for filtering keys by state, and automatic real-time broadcasting when status changes.
Checkout Workflow Implementation
The checkout process manages state transitions carefully:
# app/models/key_checkout.rb
class KeyCheckout < ApplicationRecord
belongs_to :property_key
belongs_to :user
belongs_to :checked_out_by, class_name: 'User'
belongs_to :returned_by, class_name: 'User', optional: true
validates :checked_out_at, presence: true
validates :user, presence: true
validate :key_must_be_available, on: :create
after_create :mark_key_as_checked_out
after_update :mark_key_as_available, if: :returned?
scope :active, -> { where(returned_at: nil) }
scope :overdue, -> {
active
.where('expected_return_at < ?', Time.current)
}
def returned?
returned_at.present?
end
def overdue?
return false if returned?
expected_return_at.present? && expected_return_at < Time.current
end
def days_overdue
return 0 unless overdue?
((Time.current - expected_return_at) / 1.day).ceil
end
private
def key_must_be_available
unless property_key.available?
errors.add(:property_key, 'is not available for checkout')
end
end
def mark_key_as_checked_out
property_key.update!(status: 'checked_out')
KeyCheckoutMailer.checkout_confirmation(self).deliver_later
end
def mark_key_as_available
property_key.update!(status: 'available')
KeyCheckoutMailer.return_confirmation(self).deliver_later
end
end
This model enforces business rules: keys cannot be checked out twice simultaneously, returns update availability automatically, and email notifications send asynchronously avoiding request delays.
Checkout Controller
The controller handles user interactions:
# app/controllers/key_checkouts_controller.rb
class KeyCheckoutsController < ApplicationController
before_action :set_property_key
def create
@checkout = @property_key.key_checkouts.build(checkout_params)
@checkout.checked_out_by = current_user
@checkout.checked_out_at = Time.current
if @checkout.save
redirect_to property_path(@property_key.property),
notice: "Key checked out successfully to #{@checkout.user.name}"
else
redirect_to property_path(@property_key.property),
alert: "Failed to checkout key: #{@checkout.errors.full_messages.join(', ')}"
end
end
def return
@checkout = @property_key.current_checkout
unless @checkout
redirect_to property_path(@property_key.property),
alert: 'This key is not currently checked out'
return
end
@checkout.returned_at = Time.current
@checkout.returned_by = current_user
@checkout.return_notes = params[:return_notes]
if @checkout.save
redirect_to property_path(@property_key.property),
notice: 'Key returned successfully'
else
redirect_to property_path(@property_key.property),
alert: "Failed to return key: #{@checkout.errors.full_messages.join(', ')}"
end
end
private
def set_property_key
@property_key = PropertyKey.find(params[:property_key_id])
end
def checkout_params
params.require(:key_checkout).permit(
:user_id,
:expected_return_at,
:checkout_reason,
:checkout_notes
)
end
end
This controller maintains simplicity: create checkouts with validation, process returns with timestamps, and redirect with clear feedback messages.
Real-Time Updates with ActionCable
Key status changes broadcast to all viewing staff:
// app/javascript/controllers/property_keys_controller.js
import consumer from '../channels/consumer'
export function propertyKeys(propertyId) {
return {
keys: [],
subscription: null,
init() {
this.loadKeys()
this.subscribeToUpdates()
},
destroy() {
if (this.subscription) {
this.subscription.unsubscribe()
}
},
async loadKeys() {
const response = await fetch(`/properties/${propertyId}/keys.json`)
this.keys = await response.json()
},
subscribeToUpdates() {
this.subscription = consumer.subscriptions.create(
{
channel: 'PropertyKeysChannel',
property_id: propertyId
},
{
received: (data) => {
this.handleKeyUpdate(data)
}
}
)
},
handleKeyUpdate(data) {
switch (data.event) {
case 'key_status_changed':
this.updateKeyStatus(data)
break
case 'key_added':
this.addKey(data.key)
break
}
},
updateKeyStatus(data) {
const key = this.keys.find(k => k.id === data.key_id)
if (key) {
key.status = data.status
key.current_holder = data.current_holder
key.expected_return = data.expected_return
this.flashUpdateIndicator(key)
}
},
addKey(keyData) {
this.keys.push(keyData)
},
flashUpdateIndicator(key) {
// Visual feedback showing which key updated
const element = this.$refs[`key_${key.id}`]
if (element) {
element.classList.add('flash-update')
setTimeout(() => {
element.classList.remove('flash-update')
}, 1000)
}
},
statusBadgeClass(key) {
if (key.status === 'available') return 'badge-success'
if (key.expected_return && new Date(key.expected_return) < new Date()) {
return 'badge-danger'
}
return 'badge-warning'
}
}
}
This Alpine.js component maintains reactive key state, receives WebSocket updates, and provides visual feedback when changes occur showing staff what updated without requiring manual refresh.
Automated Overdue Reminders
Background jobs handle reminder emails:
# app/jobs/overdue_key_reminder_job.rb
class OverdueKeyReminderJob < ApplicationJob
queue_as :default
def perform
KeyCheckout.overdue.find_each do |checkout|
KeyCheckoutMailer.overdue_reminder(checkout).deliver_now
end
end
end
# config/schedule.rb (using whenever gem)
every 1.day, at: '9:00 am' do
runner "OverdueKeyReminderJob.perform_later"
end
# app/mailers/key_checkout_mailer.rb
class KeyCheckoutMailer < ApplicationMailer
def overdue_reminder(checkout)
@checkout = checkout
@property = checkout.property_key.property
@days_overdue = checkout.days_overdue
mail(
to: @checkout.user.email,
subject: "Reminder: Overdue keys for #{@property.display_address}"
)
end
def checkout_confirmation(checkout)
@checkout = checkout
@property = checkout.property_key.property
mail(
to: @checkout.user.email,
subject: "Key checkout confirmation: #{@property.display_address}"
)
end
def return_confirmation(checkout)
@checkout = checkout
@property = checkout.property_key.property
mail(
to: @checkout.user.email,
subject: "Key return confirmed: #{@property.display_address}"
)
end
end
These automated reminders run daily, identifying overdue checkouts and sending gentle nudges encouraging returns without requiring manual intervention.
Testing Key Management
Testing verifies checkout business logic:
RSpec.describe PropertyKey do
let(:property) { create(:property) }
let(:key) { create(:property_key, property: property) }
let(:user) { create(:user) }
describe '#available?' do
it 'returns true when status is available and no current checkout' do
expect(key.available?).to be true
end
it 'returns false when checked out' do
create(:key_checkout, property_key: key, user: user)
key.update!(status: 'checked_out')
expect(key.available?).to be false
end
end
describe '#overdue?' do
it 'returns true when checkout past expected return' do
create(:key_checkout,
property_key: key,
user: user,
expected_return_at: 1.day.ago)
key.update!(status: 'checked_out')
expect(key.overdue?).to be true
end
it 'returns false for returned checkouts' do
create(:key_checkout,
property_key: key,
user: user,
expected_return_at: 1.day.ago,
returned_at: Time.current)
expect(key.overdue?).to be false
end
end
end
RSpec.describe KeyCheckout do
let(:key) { create(:property_key) }
let(:user) { create(:user) }
describe 'validations' do
it 'prevents checkout of unavailable key' do
key.update!(status: 'checked_out')
checkout = build(:key_checkout, property_key: key, user: user)
expect(checkout).not_to be_valid
expect(checkout.errors[:property_key]).to include('is not available for checkout')
end
end
describe 'callbacks' do
it 'marks key as checked out after creation' do
create(:key_checkout, property_key: key, user: user)
expect(key.reload.status).to eq('checked_out')
end
it 'marks key as available after return' do
checkout = create(:key_checkout, property_key: key, user: user)
checkout.update!(returned_at: Time.current)
expect(key.reload.status).to eq('available')
end
end
end
These tests verify business logic operates correctly: availability checks work accurately, overdue detection functions properly, and state transitions update keys appropriately.
What's Next
The key management foundation enables sophisticated features: QR code generation for each key allowing mobile checkout via camera scan without manual selection, physical keybox integration using Bluetooth or NFC for automated checkout when staff remove keys from secure storage, geolocation tracking recording where checkouts and returns occur providing audit trail of key movements, and predictive analytics identifying keys frequently overdue suggesting process improvements.
Future enhancements might include key set management grouping related keys (front door, back door, garage) for simultaneous checkout, maintenance scheduling triggering automatic key checkouts to engineers before appointments, landlord visibility showing key status in owner portals for transparency, and insurance integration providing key tracking documentation for security compliance requirements.
By implementing comprehensive key management with real-time tracking and automated reminders, LetAdmin provides accountability and visibility for physical assets, reducing lost keys whilst maintaining complete audit trails for security and compliance purposes.
