Pricing in self-storage is far more nuanced than most people outside the industry realize. When I started building billing software for storage facilities, I assumed pricing would be the simple part. I was wrong. The gap between how a facility thinks about pricing and how that pricing actually gets implemented in code is wide, and bridging it wrong costs real money. Let me walk through the three dominant models and how each one actually works under the hood.

Flat-Rate Pricing

Flat-rate is exactly what it sounds like: every unit of a given size costs the same, always. A 10x10 is $120/month regardless of floor, climate control, or demand. This model is common at older facilities and family-owned operations that prefer simplicity over optimization.

From a billing code perspective, flat-rate is trivial to implement. The unit type carries a single base_rate, and that's the rental amount. No lookups, no calculations, no dynamic adjustments.

function getUnitPrice(Unit $unit, Facility $facility): float
{
    // Flat-rate: price lives on the unit type
    return (float) $unit->unitType->base_rate;
}

The problem with flat-rate becomes apparent during high-demand periods. When your 10x10 units are 95% occupied and people are driving past your facility to pay more at a competitor, your flat-rate model has no mechanism to capture that value. Conversely, during slow periods, flat-rate gives you no automated lever to move inventory. You're dependent entirely on promotions and manual rate changes.

Where flat-rate does shine is in rate-change auditing. Every tenant on a given unit type pays the same thing, so rate disputes are easy to resolve. When you have dynamic pricing, explaining to a tenant why they pay $135 and their neighbor pays $115 for the same size unit requires more careful customer communication tooling.

Tiered Pricing

Tiered pricing introduces premium modifiers based on unit attributes: floor level, climate control, drive-up access, proximity to elevator, unit dimensions (a 10x12 versus a 10x10), and sometimes lease term. Each attribute carries a modifier, and the final price is the base rate plus the sum of applicable modifiers.

function getUnitPrice(Unit $unit, Facility $facility): float
{
    $baseRate = (float) $unit->unitType->base_rate;
    $modifiers = [];

    // Climate control premium
    if ($unit->is_climate_controlled) {
        $modifiers[] = $facility->pricing_config->climate_premium ?? 0;
    }

    // Floor surcharge or discount
    $floorModifiers = $facility->pricing_config->floor_modifiers ?? [];
    if (isset($floorModifiers[$unit->floor])) {
        $modifiers[] = $floorModifiers[$unit->floor];
    }

    // Drive-up premium
    if ($unit->is_drive_up) {
        $modifiers[] = $facility->pricing_config->drive_up_premium ?? 0;
    }

    // Long-term lease discount (modifier will be negative)
    if (isset($unit->current_lease) && $unit->current_lease->term_months >= 12) {
        $modifiers[] = $facility->pricing_config->annual_lease_discount ?? 0;
    }

    $totalModifier = array_sum($modifiers);

    // Modifiers can be flat amounts or percentages depending on config type
    return $facility->pricing_config->modifier_type === 'percentage'
        ? round($baseRate * (1 + $totalModifier / 100), 2)
        : round($baseRate + $totalModifier, 2);
}

Tiered pricing requires careful schema design. I maintain a pricing_modifiers table linked to the facility, with columns for modifier_type, attribute_key, attribute_value, and modifier_amount. This lets facility managers adjust modifiers without a code deploy, and it gives you a full history of when modifiers changed — which matters when a tenant asks why their rate changed.

Dynamic / Street-Rate Pricing

Street-rate pricing is where self-storage revenue management gets serious. The concept is borrowed from hotel and airline revenue management: prices fluctuate based on occupancy, demand signals, competitor rates, and seasonal factors. Most enterprise storage platforms either include this natively (SiteLink Web Edition has a rate management module; storEDGE integrates with Prorize) or support it via API-driven rate updates.

The billing system's job is not to calculate the dynamic price — that's a revenue management engine's job — but to consume the current street rate at move-in time and lock it as the tenant's contract rate. The key architecture decision is: when does the billing system look up the current rate, and when does it use the locked contract rate?

function getUnitPrice(Unit $unit, Facility $facility, ?Lease $activeLease = null): float
{
    // Existing tenants pay their locked contract rate
    if ($activeLease !== null && $activeLease->status === 'active') {
        return (float) $activeLease->contract_rate;
    }

    // New move-in: fetch current street rate
    return fetchCurrentStreetRate($unit, $facility);
}

function fetchCurrentStreetRate(Unit $unit, Facility $facility): float
{
    // Check for a cached/pushed rate from revenue management engine
    $stmt = $facility->db->prepare("
        SELECT rate
        FROM unit_street_rates
        WHERE unit_type_id = ?
          AND facility_id = ?
          AND effective_date <= CURDATE()
        ORDER BY effective_date DESC
        LIMIT 1
    ");
    $stmt->execute([$unit->unit_type_id, $facility->id]);
    $rate = $stmt->fetchColumn();

    // Fall back to base rate if no street rate is set
    return $rate !== false ? (float) $rate : (float) $unit->unitType->base_rate;
}

Integration with SiteLink and storEDGE

If you're building a billing layer that sits alongside SiteLink Web Edition, you'll interact with rates primarily through the SiteLink Web Services SOAP API. The GetUnitTypes method returns current rates per unit type. When your billing engine needs to verify a rate at move-in, call this endpoint rather than trusting a locally cached value — SiteLink's rate management may have updated the rate since your last sync.

For storEDGE, the REST API exposes a /units/{id}/pricing endpoint that returns the current market rate along with any active promotions. The critical thing to handle here is the difference between the advertised market rate and the effective move-in rate after promotions — these are often different values, and your contract needs to reflect the actual charged rate, not the street rate.

// storEDGE-style pricing response normalization
function normalizePricingResponse(array $apiResponse): array
{
    return [
        'street_rate'      => (float) $apiResponse['market_rate'],
        'effective_rate'   => (float) ($apiResponse['promotional_rate'] ?? $apiResponse['market_rate']),
        'promotion_id'     => $apiResponse['active_promotion']['id'] ?? null,
        'promotion_expires'=> $apiResponse['active_promotion']['expires_at'] ?? null,
    ];
}

Revenue Management Considerations

Regardless of which pricing model a facility uses at move-in, most mature operators want to implement periodic rate increases for existing tenants. The billing system needs to track the gap between a tenant's contract rate and the current street rate, and provide tooling to send rate increase notices with the legally required notice period (typically 30 days, but this varies by state and lease terms).

A rate increase queue that works the same way as the renewal queue — staged entries, status tracking, automated notice generation — handles this cleanly. The key metric to expose in the admin interface is "rate differential": the percentage gap between a tenant's current contract rate and today's street rate for their unit type. Tenants with the largest rate differential are your best candidates for an increase notice, and surfacing that in a sortable table saves facility managers hours of manual analysis.

Each pricing model has a place in the ecosystem. Flat-rate is operationally simple but leaves money on the table. Tiered pricing captures unit-level value without complexity. Dynamic pricing maximizes revenue but demands investment in revenue management infrastructure and customer communication tooling to handle the inevitable "why did my rate change" calls. Build your billing system to support all three, configurable per facility, and you'll be ready for wherever a client's operation sits on that maturity curve.