Skip to main content

Building a Complete App

Build a customer support ticket system from zero to deployed, with users, roles, business rules, views, API access, and a knowledge base. Every tool call is real.

What you'll build:

  • Two entities: tickets and agents
  • Business rules for validation and defaults
  • RBAC with two roles: support agent and manager
  • Filtered views for dashboards
  • REST API access via platform keys
  • A deployed frontend
  • A knowledge base for RAG-powered support search

Phase 1: Data Model​

Start by selecting your tenant (see MCP Quickstart if you haven't connected yet):

select_tenant({ tenantSlug: "support-app" })

Create the "agents" entity​

Agents handle tickets. Create this first because tickets will reference it.

generate_entity({
name: "agents",
fields: [
{ "name": "name", "type": "text", "required": true },
{ "name": "email", "type": "text", "required": true },
{ "name": "department", "type": "select", "options": ["billing", "technical", "general"] }
]
})

Publish it:

publish_entity({
entityName: "agents",
message: "Initial agents entity"
})

Create the "tickets" entity​

generate_entity({
name: "tickets",
fields: [
{ "name": "title", "type": "text", "required": true },
{ "name": "description", "type": "long_text" },
{ "name": "status", "type": "select", "options": ["open", "in_progress", "resolved"] },
{ "name": "priority", "type": "select", "options": ["low", "medium", "high", "critical"] },
{ "name": "customer_email", "type": "text", "required": true },
{ "name": "assigned_to", "type": "relation", "relatedEntity": "agents" }
]
})

Publish it:

publish_entity({
entityName: "tickets",
message: "Initial tickets entity with agent relation"
})

Add some test data​

create_record({
entityName: "agents",
data: {
"name": "Alice Chen",
"email": "alice@support.com",
"department": "technical"
}
})

Note the agent record ID from the response (e.g., agent-uuid-1), then create a ticket:

create_record({
entityName: "tickets",
data: {
"title": "Cannot reset password",
"description": "Customer clicks reset link but gets a 404 page",
"status": "open",
"priority": "high",
"customer_email": "customer@example.com",
"assigned_to": "agent-uuid-1"
}
})

Phase 2: Business Rules​

Rule 1: Title is required​

This is already handled by required: true on the field definition. But let's add a rule for priority validation:

generate_business_rule({
entityName: "tickets",
prompt: "Validate that priority must be one of: low, medium, high, critical. Reject the record if it's not."
})

Response:

{
"success": true,
"rule": {
"id": "rule-uuid-1",
"name": "validate-priority-values",
"entityName": "tickets",
"trigger": "on_create",
"status": "draft",
"dsl": {
"validate": [
{
"condition": "priority != 'low' and priority != 'medium' and priority != 'high' and priority != 'critical'",
"action": "reject",
"message": "Priority must be one of: low, medium, high, critical"
}
]
}
}
}

Publish it:

publish_business_rule({ ruleId: "rule-uuid-1" })

Rule 2: Default status to "open" on create​

generate_business_rule({
entityName: "tickets",
prompt: "When a ticket is created, if status is not set, default it to 'open'"
})

Publish:

publish_business_rule({ ruleId: "rule-uuid-2" })

Now any new ticket without a status automatically gets status: "open".

Phase 3: Users & RBAC​

Create roles​

Support agent -- can manage tickets, read agents:

create_role({
name: "support_agent",
description: "Can create, read, update tickets. Read-only on agents.",
permissions: {
entities: {
"tickets": ["create", "read", "update"],
"agents": ["read"]
}
}
})

Response:

{
"success": true,
"role": {
"id": "role-uuid-1",
"name": "support_agent",
"description": "Can create, read, update tickets. Read-only on agents."
}
}

Support manager -- full access to both entities plus user management:

create_role({
name: "support_manager",
description: "Full access to tickets and agents. Can manage users.",
permissions: {
entities: {
"tickets": ["create", "read", "update", "delete"],
"agents": ["create", "read", "update", "delete"]
},
canManageUsers: true
}
})

Create users​

create_user({
tenantSlug: "support-app",
email: "agent1@support.com",
password: "securepass123",
name: "Agent One",
role: "member"
})

Note the user ID from the response, then assign the role:

assign_role({
userId: "user-uuid-1",
roleName: "support_agent"
})

Create a manager:

create_user({
tenantSlug: "support-app",
email: "manager@support.com",
password: "securepass456",
name: "Manager User",
role: "admin"
})
assign_role({
userId: "user-uuid-2",
roleName: "support_manager"
})

Verify access​

Log in as the agent to test permissions:

tenant_login({
tenantSlug: "support-app",
email: "agent1@support.com",
password: "securepass123"
})

The returned JWT can be used with the REST API. The agent will be able to read tickets and agents, create/update tickets, but not delete anything.

Phase 4: Views​

Views are filtered projections of an entity with their own permissions. They power dashboards.

View: "my-tickets"​

Shows only tickets assigned to the current user:

create_view({
entitySlug: "tickets",
slug: "my-tickets",
name: "My Tickets",
description: "Tickets assigned to the current user",
filterDsl: {
validate: [
{ condition: "assigned_to == $currentUser" }
]
}
})

Response:

{
"success": true,
"view": {
"slug": "my-tickets",
"name": "My Tickets",
"entitySlug": "tickets",
"isActive": true
}
}

View: "critical-open"​

Shows all critical tickets that are still open:

create_view({
entitySlug: "tickets",
slug: "critical-open",
name: "Critical Open Tickets",
description: "All critical tickets with open status",
filterDsl: {
validate: [
{ condition: "priority == 'critical' and status == 'open'" }
]
}
})

Grant view access to the support_agent role​

Views use the permission key format view:<slug>. To let agents see their own tickets through the view, update the role or create a new one that includes view access:

create_role({
name: "support_agent_v2",
description: "Can manage tickets, read agents, access views",
permissions: {
entities: {
"tickets": ["create", "read", "update"],
"agents": ["read"],
"view:my-tickets": ["read"],
"view:critical-open": ["read"]
}
}
})

Query through a view​

Via REST API:

curl -H "Authorization: Bearer JWT_TOKEN" \
"https://api.fyso.dev/api/views/my-tickets/records"

The view's base filter is applied automatically. You can add extra filters on top:

curl -H "Authorization: Bearer JWT_TOKEN" \
"https://api.fyso.dev/api/views/my-tickets/records?filter.priority=high&sort=createdAt&order=desc"

Admins (API key access) bypass the user filter and see all records through the view.

Phase 5: API Access​

Generate the OpenAPI spec​

get_rest_api_spec()

Returns the full OpenAPI specification for your tenant's entities, including all endpoints and schemas. You can paste this into Swagger UI or import it into Postman.

REST API examples​

List tickets with filters:

curl -H "Authorization: Bearer API_KEY" \
"https://api.fyso.dev/api/entities/tickets/records?filter.status=open&filter.priority=critical&sort=createdAt&order=desc&limit=20"

Create a ticket from an external system:

curl -X POST "https://api.fyso.dev/api/entities/tickets/records" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Billing issue",
"description": "Customer charged twice for subscription",
"priority": "high",
"customer_email": "upset@customer.com"
}'

Update a ticket:

curl -X PUT "https://api.fyso.dev/api/entities/tickets/records/RECORD_ID" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "resolved"}'

Generate an API client​

generate_api_client({
language: "typescript"
})

Returns a complete TypeScript client with types for your entities.

Public key access​

For external integrations that need limited access (e.g., a customer-facing form that creates tickets), use public keys. Public keys are scoped to specific entities and operations. See Public Keys for setup.

Phase 6: Deploy​

Deploy a frontend​

Build your frontend (React, Vue, Astro, plain HTML -- anything that compiles to static files), then deploy:

deploy_static_site({
subdomain: "support-app",
path: "/path/to/your/frontend/dist"
})

Response:

{
"success": true,
"message": "Site deployed successfully",
"data": {
"url": "https://support-app-sites.fyso.dev",
"subdomain": "support-app"
}
}

Your app is live at https://support-app-sites.fyso.dev.

Set up CI/CD​

Generate a deploy token for automated deployments:

generate_deploy_token({ subdomain: "support-app" })

Use this token in your GitHub Actions workflow. See GitHub Actions Deployment for the full setup.

Custom domain (Pro)​

Point your own domain to the site:

set_custom_domain({
subdomain: "support-app",
action: "add",
domain: "support.mycompany.com"
})

Follow the DNS instructions in the response (add a CNAME record), then verify:

set_custom_domain({
subdomain: "support-app",
action: "verify"
})

Once verified, your site is accessible at both support-app-sites.fyso.dev and support.mycompany.com with automatic SSL.

Phase 7: Knowledge Base (Optional)​

Add a searchable knowledge base so your support agents can find answers from documentation using semantic search.

Upload support documents​

Upload a PDF manual:

upload_document({
filePath: "/path/to/support-handbook.pdf",
title: "Support Handbook 2026"
})

Upload content from a URL:

upload_document({
title: "FAQ Page",
content: "https://mycompany.com/faq",
source_type: "url"
})

Upload inline text:

upload_document({
title: "Password Reset Procedure",
content: "## Password Reset\n\n1. Go to Settings > Security\n2. Click 'Reset Password'\n3. Enter current password\n4. Enter and confirm new password\n5. Click Save\n\nPasswords must be at least 8 characters with one uppercase letter and one number.",
source_type: "text"
})

Documents are automatically chunked and indexed. Status goes from processing to ready.

Search the knowledge base​

search_knowledge({
query: "How does a customer reset their password?",
limit: 5,
threshold: 0.3,
one_per_document: true
})

Response:

{
"success": true,
"data": {
"results": [
{
"content": "## Password Reset\n\n1. Go to Settings > Security\n2. Click 'Reset Password'...",
"score": 0.91,
"document": {
"id": "doc-uuid",
"title": "Password Reset Procedure",
"source_type": "text"
},
"chunk_index": 0,
"token_count": 82
}
],
"query_time_ms": 38
}
}

Search works by meaning, not keyword matching. "How does a customer reset their password?" matches content about password resets even if the exact words differ.

Check stats​

get_knowledge_stats()

Returns document counts, search analytics, and embedding token usage. Use this to monitor search quality -- watch the zero_result_rate and avg_score fields.


Recap​

Here's everything you built:

LayerWhat you did
Data modelTwo entities (tickets, agents) with a relation
Business rulesPriority validation + default status on create
RBACTwo roles with scoped permissions
ViewsFiltered dashboards (my-tickets, critical-open)
APIREST endpoints + generated client
DeployStatic site on support-app-sites.fyso.dev
KnowledgeSearchable docs for RAG-powered support

All of this was done through MCP tool calls. No code deployment, no infrastructure setup. The REST API and views are ready for your frontend to consume.