Build a Custom Restaurant Booking Engine Without OpenTable Fees
I've watched restaurant owners do the math on their OpenTable invoices and physically wince. A busy bar doing 1,000 covers a month is hemorrhaging $18,000+ per year just so guests can click a "Reserve" button. That's a line cook's salary. That's a patio renovation. That's money walking out the door every single month to a platform that also uses your customer data to market other restaurants to your guests.
The good news? Building a custom booking engine isn't the massive undertaking it was five years ago. Modern web frameworks, hosted databases, and a handful of smart integrations mean you can have a production-ready reservation system running on your own domain for a fraction of what you're paying OpenTable annually. I've helped build several of these for restaurant and bar clients, and I'm going to walk you through exactly how it works.

Table of Contents
- The Real Cost of OpenTable in 2025
- Off-the-Shelf Alternatives Worth Considering First
- When a Custom Build Makes Sense
- Architecture of a Restaurant Booking Engine
- Building the Frontend Widget
- Backend: Availability Engine and Conflict Resolution
- Table Management and Floor Plans
- Notifications, Reminders, and No-Show Reduction
- Payment Deposits and Cancellation Policies
- Google Reserve Integration
- Deployment, Hosting, and Ongoing Costs
- Real Cost Comparison: Custom vs. OpenTable vs. Alternatives
- FAQ
The Real Cost of OpenTable in 2025
Let's put actual numbers on the table (pun intended). OpenTable's pricing in 2025 works like this:
- Setup fee: $1,200+
- Monthly subscription: $249/month
- Per-cover fee: $1.00 for reservations made through your own site, $2.50 for reservations made through OpenTable's network
- Annual cost for a restaurant averaging 1,000 covers/month: roughly $15,000-$18,000/year
That per-cover model is the killer. The busier you get, the more you pay. It's a tax on your own success. And here's the part that really stings: OpenTable owns the customer relationship data. They'll use your guests' dining history to suggest competitors. You're essentially paying a middleman to build a database they use against you.
For a single-location bar or restaurant, that $18K/year is brutal. For a multi-location group? Multiply accordingly.
Off-the-Shelf Alternatives Worth Considering First
Before you commit to a custom build, be honest about whether an existing platform solves your problem. The market has shifted dramatically toward flat-fee and free models. Here's what the landscape looks like in 2025:
| Platform | Free Tier | Paid Pricing | Per-Cover Fee | Google Integration | Key Limitation |
|---|---|---|---|---|---|
| Resos | 25 bookings/month | $24/month (flat) | None | Yes | Free tier is tiny |
| GloriaFood | Unlimited bookings | Free core + add-ons | None | Limited | Minimal customization |
| Tablesit | 500 bookings/month | Not published | None | Yes | No SMS on free tier |
| Anolla | Basic features | Modular add-ons | None | Yes | Free lacks key modules |
| Sagenda | Fully free | N/A | None | No | No real table management |
| Tableo | 100 covers | ~$75/month | None | Yes (Reserve) | Limited free features |
| Tablein | N/A | Fixed monthly | None | Yes | Aimed at smaller venues |
| Eveve | N/A | $150-$300/month | None | Yes | Price varies by location |
If you're a small bar doing under 500 reservations a month, Tablesit or Resos might genuinely be all you need. GloriaFood is solid if you also want online ordering baked in. These tools have gotten surprisingly good.
But they all share common limitations: you're still on someone else's platform, your customization options are limited, you can't deeply integrate with your existing tech stack, and you don't own the infrastructure. For many restaurants, that's fine. For others, it's not.

When a Custom Build Makes Sense
A custom reservation system makes sense when:
- You're a multi-location group and need centralized management with location-specific logic
- You have an existing website built on a modern stack (Next.js, Astro, etc.) and want the booking experience to feel native, not like an embedded iframe from 2014
- You need custom business logic -- different booking rules for the bar vs. the dining room, event-based availability, seasonal menus tied to reservation slots
- You want to own your data completely, with no third-party access to your customer database
- You're spending $10K+/year on OpenTable and the custom build pays for itself within 12-18 months
- You want integration with your existing POS, CRM, or marketing tools that off-the-shelf platforms don't support
If three or more of those apply, keep reading. We build these kinds of systems regularly as part of our headless CMS development work, and the ROI conversation is almost always straightforward.
Architecture of a Restaurant Booking Engine
Here's the high-level architecture I'd recommend for a modern custom booking engine:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Frontend Widget │────▶│ API Layer │────▶│ Database │
│ (React/Astro) │ │ (Node/Express) │ │ (PostgreSQL) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
│ ├──▶ Twilio (SMS) │
│ ├──▶ SendGrid (Email) │
│ ├──▶ Stripe (Deposits) │
│ ├──▶ Google Calendar │
│ └──▶ POS Integration │
│ │
┌─────────────────┐ ┌─────────────────┐
│ Admin Dashboard │──────────────────────────────▶│ Same API/DB │
│ (Staff portal) │ │ │
└─────────────────┘ └─────────────────┘
The frontend widget lives on your restaurant's website. The API handles all business logic -- availability checks, conflict resolution, notification triggers. PostgreSQL stores everything: reservations, floor plans, customer profiles, preferences. External services handle the stuff you don't want to build from scratch.
Building the Frontend Widget
The booking widget is what your guests interact with. It needs to be fast, mobile-first (over 70% of restaurant reservations happen on phones), and dead simple.
Here's a simplified React component for the core booking form:
import { useState } from 'react';
export function BookingWidget({ restaurantId }: { restaurantId: string }) {
const [date, setDate] = useState('');
const [time, setTime] = useState('');
const [partySize, setPartySize] = useState(2);
const [availableSlots, setAvailableSlots] = useState([]);
async function checkAvailability() {
const res = await fetch(`/api/availability`, {
method: 'POST',
body: JSON.stringify({ restaurantId, date, partySize }),
});
const data = await res.json();
setAvailableSlots(data.slots);
}
async function confirmBooking() {
const res = await fetch(`/api/reservations`, {
method: 'POST',
body: JSON.stringify({
restaurantId, date, time, partySize,
// guest details collected in a previous step
}),
});
// Handle confirmation, redirect to success page
}
return (
<div className="booking-widget">
<input type="date" onChange={(e) => setDate(e.target.value)} />
<select onChange={(e) => setPartySize(Number(e.target.value))}>
{[1,2,3,4,5,6,7,8].map(n => (
<option key={n} value={n}>{n} {n === 1 ? 'guest' : 'guests'}</option>
))}
</select>
<button onClick={checkAvailability}>Check Availability</button>
{availableSlots.map(slot => (
<button key={slot.time} onClick={() => { setTime(slot.time); confirmBooking(); }}>
{slot.time}
</button>
))}
</div>
);
}
This is obviously simplified -- you'll want proper form validation, loading states, error handling, and a multi-step flow that collects guest name, email, phone, and special requests. But the core interaction is straightforward: pick a date, pick a party size, see available times, book one.
For restaurants running on Next.js (which we build extensively -- see our Next.js development capabilities), the widget becomes a server component that can pre-fetch availability data. For static sites built with Astro, you'd use a client-side island for the interactive booking form while keeping the rest of the page statically generated for maximum performance.
Backend: Availability Engine and Conflict Resolution
This is where the real complexity lives. The availability engine needs to answer one question quickly and accurately: "Given this date, time, and party size, which tables are available?"
Here's the core database schema:
CREATE TABLE tables (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
restaurant_id UUID REFERENCES restaurants(id),
label VARCHAR(50), -- "Table 1", "Bar Seat 3"
zone VARCHAR(50), -- "patio", "bar", "main_dining"
min_capacity INT NOT NULL,
max_capacity INT NOT NULL,
is_active BOOLEAN DEFAULT true,
position_x FLOAT, -- for floor plan rendering
position_y FLOAT
);
CREATE TABLE reservations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
restaurant_id UUID REFERENCES restaurants(id),
table_id UUID REFERENCES tables(id),
guest_name VARCHAR(255) NOT NULL,
guest_email VARCHAR(255),
guest_phone VARCHAR(50),
party_size INT NOT NULL,
date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL, -- calculated from dining duration
status VARCHAR(20) DEFAULT 'confirmed', -- confirmed, seated, completed, cancelled, no_show
notes TEXT,
deposit_amount DECIMAL(10,2) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE booking_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
restaurant_id UUID REFERENCES restaurants(id),
zone VARCHAR(50),
day_of_week INT, -- 0=Sunday, 6=Saturday
first_slot TIME,
last_slot TIME,
slot_interval_minutes INT DEFAULT 15,
dining_duration_minutes INT DEFAULT 90,
buffer_minutes INT DEFAULT 15,
max_covers_per_slot INT
);
The availability check query needs to find tables that:
- Fit the party size
- Aren't already booked during the requested time window (including buffer)
- Are in an active zone for that day/time
SELECT t.id, t.label, t.zone
FROM tables t
WHERE t.restaurant_id = $1
AND t.is_active = true
AND t.min_capacity <= $2 -- party size
AND t.max_capacity >= $2
AND t.id NOT IN (
SELECT r.table_id FROM reservations r
WHERE r.date = $3
AND r.status NOT IN ('cancelled', 'no_show')
AND r.start_time < ($4::TIME + ($5 || ' minutes')::INTERVAL) -- requested end
AND r.end_time > ($4::TIME - ($6 || ' minutes')::INTERVAL) -- buffer before
)
ORDER BY t.max_capacity ASC; -- prefer smallest suitable table
That last ORDER BY is important -- you always want to assign the smallest table that fits the party. Seating a couple at a six-top during Friday dinner service is a great way to lose money.
The buffer time between reservations is critical. I usually recommend 15 minutes for casual spots, 30 minutes for fine dining. It accounts for table clearing, resetting, and the inevitable party that lingers over dessert.
Table Management and Floor Plans
Staff need to see the floor at a glance. The admin dashboard should render an interactive floor plan using SVG or HTML Canvas. Each table is a draggable element positioned on a background image of the actual floor plan.
For the admin interface, I'd typically build this as a separate Next.js app (or a protected route within the main site) with role-based access. The host sees tonight's reservations and can drag-and-drop to reassign tables. The manager sees analytics and configuration.
Storing table positions as position_x and position_y floats in the database means the floor plan is fully customizable. Import a photo of your actual restaurant layout, position the tables on top, and you've got a visual management tool that mirrors your physical space.
Notifications, Reminders, and No-Show Reduction
Automated notifications aren't optional -- they're how you reduce no-shows by 20-30%. Here's the notification flow:
- Instant confirmation -- Email + SMS as soon as the booking is made
- 24-hour reminder -- SMS asking the guest to confirm or cancel
- 2-hour reminder -- Optional, works well for dinner service
- Post-visit follow-up -- Email thanking them, asking for a review, inviting them back
Twilio handles SMS at roughly $0.0079 per message in the US. SendGrid's free tier covers 100 emails/day, which is plenty for most single-location restaurants. Even at scale, you're looking at $20-50/month for both services combined.
Here's a simple cron job pattern for the reminder system:
// Run every hour via cron
async function sendReminders() {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const upcomingReservations = await db.query(
`SELECT r.*, g.phone, g.email
FROM reservations r
WHERE r.date = $1
AND r.status = 'confirmed'
AND r.reminder_sent = false`,
[tomorrow.toISOString().split('T')[0]]
);
for (const res of upcomingReservations.rows) {
await twilioClient.messages.create({
body: `Reminder: Your reservation at ${RESTAURANT_NAME} tomorrow at ${res.start_time} for ${res.party_size}. Reply C to confirm or X to cancel.`,
to: res.phone,
from: TWILIO_NUMBER,
});
await db.query(
'UPDATE reservations SET reminder_sent = true WHERE id = $1',
[res.id]
);
}
}
Payment Deposits and Cancellation Policies
For high-demand slots (Friday/Saturday dinner, brunch, holiday events), collecting a deposit at booking time dramatically reduces no-shows. Stripe makes this trivial.
Typical deposit structures I've seen work well:
- $10-25 per person for standard dinner reservations
- Full prepayment for special events, tasting menus, or prix fixe
- No deposit for off-peak slots (you want zero friction for Tuesday lunch)
The deposit is either applied to the bill or forfeited if the guest no-shows or cancels within a window (usually 24-48 hours). Stripe's payment intents API handles the hold-and-capture flow cleanly.
Google Reserve Integration
Here's a feature most custom builds miss, and it's a big deal. Google Reserve lets guests book directly from Google Search and Google Maps. When someone searches "Italian restaurant near me" and sees your listing, they can book without ever visiting your website.
Integrating with Google Reserve requires becoming an approved booking partner or using a platform that already is (Resos, Tableo, and others have this). For a fully custom build, you'll need to implement the Google Reserve API specification, which involves exposing your availability data in a specific format that Google's systems can consume.
This is one area where the build-vs-buy decision gets real. If Google Reserve traffic is important to your restaurant (and for most urban restaurants, it absolutely is), partnering with a platform that already has this integration might make more sense than building it yourself. You can still build a custom widget for your own website while using Resos or similar specifically for the Google channel.
Deployment, Hosting, and Ongoing Costs
For a Next.js-based booking engine, Vercel is the obvious hosting choice -- the free tier handles most single-restaurant traffic easily. For the database, Supabase or Neon.tech offer generous free PostgreSQL tiers. As you scale or need more reliability, you're looking at:
- Vercel Pro: $20/month
- Supabase Pro: $25/month
- Twilio SMS: ~$20-40/month (depending on volume)
- SendGrid: Free for most volumes
- Stripe: 2.9% + $0.30 per deposit transaction (no monthly fee)
- Domain/SSL: You already have this
Total monthly hosting cost: $65-85/month. Compare that to OpenTable's $249/month before per-cover fees.
Real Cost Comparison: Custom vs. OpenTable vs. Alternatives
Let's run the numbers for a restaurant doing 1,000 covers per month:
| Solution | Year 1 Cost | Year 2 Cost | Year 3 Total | You Own the Data? |
|---|---|---|---|---|
| OpenTable | $18,000+ (setup + monthly + per-cover) | $15,000+ | $48,000+ | No |
| Resos Paid | $288 | $288 | $864 | Partially |
| Tableo Paid | ~$900 | ~$900 | $2,700 | Partially |
| Custom Build | $8,000-20,000 (dev) + $800 (hosting) | $800 (hosting) | $9,600-21,600 | Yes, 100% |
| Tablesit Free | $0 | $0 | $0 | Partially |
A custom build at the higher end ($20K development cost) breaks even versus OpenTable in 13-16 months. At the lower end ($8K), you're even by month 6. After that, it's pure savings -- $15,000+ per year that stays in your business.
The development cost varies based on complexity. A basic booking widget with email confirmations and a simple admin panel sits at the lower end. A full-featured system with floor plan management, deposit collection, POS integration, multi-location support, and analytics pushes toward the higher end.
If you're curious about what a custom build would cost for your specific situation, our pricing page has a starting point, or you can reach out directly and we'll scope it properly.
FAQ
How long does it take to build a custom restaurant reservation system?
For a minimum viable product -- booking widget, confirmation emails, basic admin panel -- expect 4-6 weeks of development time. A full-featured system with floor plan management, SMS reminders, deposit collection, and POS integration typically takes 8-12 weeks. We've shipped MVPs in as little as 3 weeks when the scope is tight and the restaurant knows exactly what they need.
Can I migrate my existing OpenTable reservation data to a custom system?
Yes, but it takes some work. OpenTable lets you export guest data (name, email, phone, visit history) as CSV files. You'll want to import this into your new system before going live so you don't lose your guest history. Some alternative platforms like Tablesit and Resos also support data imports. The critical thing is to do this before you cancel OpenTable, not after.
Will I lose reservations from Google if I leave OpenTable?
Not necessarily. Google Reserve works with multiple booking partners, not just OpenTable. Platforms like Resos and Tableo have Google Reserve integration built in. If you're building fully custom, you can still appear in Google search results with a "Reserve" button by implementing the Google Reserve API or using a hybrid approach -- custom widget on your site, third-party platform for the Google channel.
How do I handle no-shows with a custom booking system?
Three proven strategies: automated SMS reminders 24 hours before (reduces no-shows by 20-30%), requiring credit card deposits for high-demand slots, and maintaining a no-show tracking system that flags repeat offenders. Your custom system can implement all three. Some restaurants also use a waitlist feature that automatically fills cancelled slots.
Is it worth building custom for a single small restaurant?
Honestly? Probably not, unless you have very specific requirements. For a single location doing under 500 covers a month, Tablesit's free tier (500 bookings/month) or Resos at $24/month will serve you well. The custom build ROI really kicks in when you're spending $10K+/year on OpenTable fees, running multiple locations, or need integration with systems that off-the-shelf platforms don't support.
What tech stack should I use for a restaurant booking engine?
I'd recommend Next.js for both the booking widget and admin dashboard, PostgreSQL for the database (reservation data is highly relational), and Vercel for hosting. For a lighter-weight approach, Astro with React islands works beautifully for the guest-facing booking widget -- fast static pages with interactive booking forms. Node.js with Express handles the API layer well. This is the stack we typically use for our Next.js and Astro client projects.
How do I handle table assignment for walk-ins alongside online reservations?
Your admin dashboard needs a real-time view of the floor. When a walk-in arrives, the host checks the dashboard, sees which tables are free (accounting for upcoming reservations and buffer times), and manually assigns one. The system should block that table from being booked online for the appropriate dining duration. This is basically the same flow OpenTable uses -- you're just running it on your own system.
Can a custom booking system integrate with my POS?
Yes, but it depends on your POS. Systems like Toast, Square, Clover, and Lightspeed all have APIs that allow reservation data to flow into the POS (so the server knows the guest name, party size, and any notes before they arrive). More advanced integrations can pull check data back into the reservation system for analytics -- average spend per cover, popular items by time slot, etc. POS integration is usually the most time-consuming part of a custom build because every POS API is different.