Appfolio reference architecture
425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill appfolio-reference-architectureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
'Reference architecture for AppFolio property management integration.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
5.3 KB, as published. Nobody here has run it
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.
Architecture Diagram
Dashboard (React) ──→ Property Service ──→ Redis Cache ──→ AppFolio Stack API
↓ /properties
Queue (Bull) ──→ Sync Worker /tenants
↓ /leases
Webhook Handler ←── AppFolio Events /work-orders
↓ /bills
Accounting Sync ──→ QuickBooks/Xero
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 Tenant { id: string; name: string; email: string; leaseId: string; balance: number; }
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
| Component | Failure Mode | Recovery |
|---|---|---|
| Property sync | AppFolio 429 rate limit | Exponential backoff with jitter, per-property circuit breaker |
| Lease webhook | Duplicate event delivery | Idempotency key on lease ID + event timestamp |
| Work order routing | Vendor API timeout | Queue retry with fallback to manual assignment |
| Accounting sync | Balance mismatch | Reconciliation queue with human review flag |
| Tenant portal | Cache miss storm | Stale-while-revalidate pattern, circuit breaker on API layer |
Resources
Next Steps
See appfolio-deploy-integration.