← All posts
PRODUCTPublished · April 15, 2026

Multi-tenant LINE agents: 5 hard lessons from BeauBot

One channel per store, prompt isolation, shared context — what we learned turning a ReAct agent into a multi-tenant SaaS.

3 min read

BeauBot is a LINE AI customer-service SaaS for beauty studios. The first version served one nail salon — a great code experience: hardcoded LINE Channel ID, hardcoded prompt, hours in a config file. Scaling to many studios surfaced 5 structural problems. Notes for whoever follows.

Lesson 1: Per-store channels — where does the token live

LINE Messaging API Channel Access Tokens are long-lived (30 days or non-expiring). Each store's token must be encrypted-at-rest. My choice:

internal/store/tenant.go
// Channel tokens encrypted with PG's pgcrypto extension
type Tenant struct {
    ID                  string `db:"id"`
    Name                string `db:"name"`
    LineChannelID       string `db:"line_channel_id"`
    // Never store raw token. Store pgcrypto-encrypted base64.
    LineChannelTokenEnc string `db:"line_channel_token_enc"`
    OperatingHours      string `db:"operating_hours"` // JSON
}
 
// Insert:
// INSERT INTO tenants (..., line_channel_token_enc)
// VALUES (..., pgp_sym_encrypt($1, $masterKey))
//
// Read:
// SELECT pgp_sym_decrypt(line_channel_token_enc::bytea, $masterKey) FROM tenants

$masterKey is read from env (AES_MASTER_KEY), never persisted.

Lesson 2: Prompt isolation vs shared template

Each store has a different tone: a young nail salon wants playful, a traditional barber wants formal. But the prompt structure is mostly the same (role + tool list + examples).

I started by giving each store a full prompt. Updating the base flow meant editing every tenant — disaster.

Switched to a layered prompt:

prompt structure
[base layer — shared]
You are a beauty-studio LINE customer-service agent. Available tools:
- query_services: list services
- check_availability: list slots
- create_booking: create a booking
...
 
[tenant layer — overridden per store]
store name: {{store_name}}
tone: {{tone}}  # casual | formal | friendly
custom rules: {{custom_rules}}
example conversation: {{examples}}

Edit the base, every store benefits. Edit a tenant layer, only that store changes.

Lesson 3: tenant context for tools

A ReAct agent's tool calls need to know "which store am I?". If a tool signature is func queryServices(category string), it has no way to know whose services to query.

Solution: partial-apply tenant ID at tool registration:

internal/agent/tools.go
func buildTools(tenantID string, db *sql.DB) []Tool {
    return []Tool{
        {
            Name: "query_services",
            Description: "List all services for this store",
            // tenantID is closed over; the LLM never sees it
            Run: func(args map[string]any) (string, error) {
                rows, err := db.Query(
                    "SELECT name, price FROM services WHERE tenant_id = $1",
                    tenantID,
                )
                // ...
            },
        },
    }
}

The LLM decides "call query_services". Tenant isolation is enforced in code, never trusted to the LLM's reasoning.

Lesson 4: per-tenant rate limit

OpenAI API has account-wide RPM / TPM limits. One store flooded with messages (a campaign launch) shouldn't throttle every other store.

Per-tenant token bucket:

internal/middleware/rate_limit.go
import "golang.org/x/time/rate"
 
var tenantLimiters sync.Map // map[tenantID]*rate.Limiter
 
func getLimiter(tenantID string) *rate.Limiter {
    if l, ok := tenantLimiters.Load(tenantID); ok {
        return l.(*rate.Limiter)
    }
    // default: 5 req/sec, burst 10
    l := rate.NewLimiter(5, 10)
    tenantLimiters.Store(tenantID, l)
    return l
}

Lesson 5: model cost control

GPT-4o costs ~US$ 0.015 per reasoning step. 300 messages/day per store × 100 stores = 30,000 calls/day = ~US$ 450/day. Won't fly.

Switched to a two-stage intent classification:

1. nano stage (GPT-4o-mini)
   classify intent: trivia / booking / complaint / other

2. dispatch by class:
   trivia → reply with nano directly (US$ 0.0003/call)
   booking → escalate to ReAct Agent on 4o
   complaint → hand off to human

60% cost reduction, no perceptible UX change.

Lessons

Multi-tenant isn't just "add a tenant_id column". Prompts, tools, rate limits, cost control — every layer needs per-tenant design. Otherwise one bad store breaks everyone.

If you're building multi-tenant AI SaaS, list these 5 issues up front. You'll save at least 2 months of refactoring.

Found it useful?

Share with someone who might benefit.