appfolio-reference-architecture

v2026.09.24

Reference architecture for AppFolio property management integration. Trigger: "appfolio architecture".

GitHub
安装命令
npx skhub add jeremylongshore/appfolio-reference-architecture
Markdown
SKILL.md

AppFolio Reference Architecture

Overview

Production architecture for property management integrations with the AppFolio Stack API. Designed for multi-property portfolios requiring real-time vacancy tracking, tenant lifecycle management, work order routing, and accounting reconciliation. Key design drivers: data freshness for leasing decisions, idempotent sync for financial accuracy, and tenant-facing portal responsiveness.

Prerequisites

  • A provider-verified contract for endpoints, authentication, events (if any), rate limits, data residency, and permitted downstream accounting/CRM effects.
  • Separate staging and production environments with managed secrets, synthetic fixtures, durable queues/idempotency stores, and named reconciliation owners.
  • Data classification that limits dashboard and cache models to the smallest permitted fields; tenant contact, payment, and balance data require separate encrypted stores and access controls.

Instructions

  1. Build the contract-bound client and safe-read service layer first, with endpoint budgets, minimized cache entries, and observability before adding write or event processing.
  2. Enable provider events only after the contract and durable raw-body, signature, persistence, and replay boundaries have been proven; otherwise use bounded incremental reconciliation.
  3. Persist an idempotency/reconciliation record before any work-order, accounting, tenant, or lease mutation and require explicit authorization.
  4. Validate the architecture in staging with synthetic data, a forced provider failure, duplicate event/retry, and rollback rehearsal before promotion.

Architecture Diagram

Dashboard (React) ──→ Property Service ──→ Redis Cache ──→ AppFolio Stack API
                           ↓                                 /properties
                      Queue (Bull) ──→ Sync Worker           /tenants
                           ↓                                 /leases
                      Event Handler ←── Provider events*      /work-orders
                           ↓                                 /bills
                      Accounting Sync ──→ QuickBooks/Xero

* Use provider events only when the active contract confirms their delivery and security semantics; otherwise feed the queue from bounded reconciliation.

Service Layer

class PropertyService {
  constructor(private client: AppFolioClient, private cache: CacheLayer) {}

  async getPortfolioSummary(propertyIds: string[]): Promise<PortfolioSummary> {
    const properties = await Promise.all(
      propertyIds.map(id => this.cache.getOrFetch(`prop:${id}`, () => this.client.get(`/properties/${id}`)))
    );
    return { totalUnits: properties.reduce((sum, p) => sum + p.units.length, 0),
             vacancyRate: this.calcVacancy(properties), pendingWorkOrders: await this.getPendingOrders(propertyIds) };
  }

  async routeWorkOrder(order: WorkOrderRequest): Promise<string> {
    const property = await this.client.get(`/properties/${order.propertyId}`);
    const vendor = this.selectVendor(property.region, order.category);
    return this.client.post('/work-orders', { ...order, assigned_vendor: vendor });
  }
}

Caching Strategy

const CACHE_CONFIG = {
  properties: { ttl: 300, prefix: 'prop' },     // 5 min — changes infrequently
  tenants:    { ttl: 120, prefix: 'tenant' },    // 2 min — moderate churn
  leases:     { ttl: 60,  prefix: 'lease' },     // 1 min — financial accuracy
  workOrders: { ttl: 30,  prefix: 'wo' },        // 30s — real-time tracking
  vacancies:  { ttl: 15,  prefix: 'vacancy' },   // 15s — leasing speed matters
};
// Webhook-driven invalidation: AppFolio events flush matching cache keys immediately

Event Pipeline

class PropertyEventPipeline {
  private queue = new Bull('appfolio-events', { redis: process.env.REDIS_URL });

  async onWebhook(event: AppFolioEvent): Promise<void> {
    await this.queue.add(event.type, event, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });
  }

  async processLeaseEvent(event: LeaseEvent): Promise<void> {
    if (event.type === 'lease.signed') await this.updateVacancy(event.propertyId);
    if (event.type === 'lease.terminated') await this.triggerMoveOutWorkflow(event);
  }

  async processWorkOrderEvent(event: WorkOrderEvent): Promise<void> {
    if (event.status === 'completed') await this.reconcileVendorInvoice(event);
  }
}

Data Model

interface Property { id: string; name: string; address: Address; units: Unit[]; region: string; }
interface TenantRef { id: string; leaseId: string; contactCiphertextRef: string; }
interface Lease    { id: string; propertyId: string; unitId: string; tenantId: string; startDate: string; endDate: string; monthlyRent: number; status: 'active' | 'pending' | 'terminated'; }
interface WorkOrder { id: string; propertyId: string; unitId: string; category: 'plumbing' | 'electrical' | 'hvac' | 'general'; status: string; assignedVendor: string; }

Scaling Considerations

  • Partition sync workers by property region to avoid cross-region API rate limits
  • Use read replicas for dashboard queries; write path goes through event pipeline
  • Batch tenant notifications (rent reminders, maintenance updates) via queue to avoid email rate limits
  • Cache vacancy data aggressively — leasing agents hit this endpoint 10x more than any other
  • Shard work order routing by property portfolio to enable independent scaling per management group

Error Handling

ComponentFailure ModeRecovery
Property syncAppFolio 429 rate limitExponential backoff with jitter, per-property circuit breaker
Lease webhookDuplicate event deliveryIdempotency key on lease ID + event timestamp
Work order routingVendor API timeoutQueue retry with fallback to manual assignment
Accounting syncBalance mismatchReconciliation queue with human review flag
Tenant portalCache miss stormStale-while-revalidate pattern, circuit breaker on API layer

Output

  • A contract-bound, staged architecture with service, cache, queue, data, and reconciliation ownership explicitly separated
  • Minimized dashboard references and encrypted/controlled boundaries for tenant contact, payment, and balance data
  • A deployment decision supported by staging failure, duplicate/replay, rate, rollback, and reconciliation evidence

Examples

For a vacancy-dashboard rollout, start with synthetic property and unit IDs, one bounded safe-read, and a cache that exposes data age. Inject a duplicate event or reconciliation record, a 429, and a downstream accounting timeout to prove the queue and idempotency store prevent duplicate effects. Promote only when results remain complete and authorized and the rollback/reconciliation owners can demonstrate their paths. If event support, data classification, durable state, or a write outcome is unverified, keep the corresponding path disabled and use operator-led reconciliation.

Resources

Next Steps

See appfolio-deploy-integration.

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/.curated/appfolio-reference-architecture

默认分支

main

最新提交

e5a6c3b

Tree SHA

c2dc8e8