Skip to main content

Overview

This portfolio uses Convex as a real-time serverless database to track blog post views. Convex provides a type-safe, reactive database that automatically syncs data between your backend and frontend.

Why Convex?

  • Real-time Updates: View counts update automatically across all connected clients
  • Type Safety: Full TypeScript support with generated types
  • Serverless: No infrastructure management required
  • Generous Free Tier: Perfect for personal portfolios
  • Zero Configuration: Deploy with a single command

Initial Setup

1

Install Convex

Install the Convex client library:
npm install convex
2

Initialize Convex

Set up Convex in your project:
npx convex dev
This command will:
  • Create a new Convex project (or connect to existing)
  • Generate configuration files
  • Set up authentication
  • Start the development server
3

Configure convex.json

Create convex.json in your project root:
convex.json
{
  "functions": "convex"
}
This tells Convex where to find your database functions.

Database Schema

Define your database schema in convex/schema.ts:
convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  blogViews: defineTable({
    slug: v.string(),
    count: v.number(),
    updatedAt: v.number(),
  }).index("by_slug", ["slug"]),
});

Schema Breakdown

FieldTypeDescription
slugStringUnique identifier for the blog post (e.g., “my-first-post”)
countNumberTotal number of views for this post
updatedAtNumberTimestamp of last view (milliseconds since epoch)
The by_slug index enables fast lookups by blog post slug, essential for real-time view tracking.

Database Functions

Implement view tracking functions in convex/views.ts:
convex/views.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";

export const incrementView = mutation({
  args: {
    slug: v.string(),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("blogViews")
      .withIndex("by_slug", (q) => q.eq("slug", args.slug))
      .unique();

    if (existing) {
      const nextCount = existing.count + 1;
      await ctx.db.patch(existing._id, {
        count: nextCount,
        updatedAt: Date.now(),
      });
      return nextCount;
    }

    await ctx.db.insert("blogViews", {
      slug: args.slug,
      count: 1,
      updatedAt: Date.now(),
    });

    return 1;
  },
});

export const getViewCount = query({
  args: {
    slug: v.string(),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("blogViews")
      .withIndex("by_slug", (q) => q.eq("slug", args.slug))
      .unique();

    return existing ? existing.count : 0;
  },
});

Function Details

incrementView (Mutation)

Increments the view count for a blog post:
  1. Queries the database for existing view record by slug
  2. If exists: increments count and updates timestamp
  3. If new: creates record with count of 1
  4. Returns the new view count
Mutations modify data and are strongly consistent. Use them for writes.

getViewCount (Query)

Retrieves the current view count for a blog post:
  1. Queries the database by slug
  2. Returns count if exists, otherwise returns 0
Queries are read-only and reactive. Components using queries automatically re-render when data changes.

Frontend Integration

Setup Convex Provider

Wrap your app with the Convex provider:
import { ConvexProvider, ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

function App({ Component, pageProps }) {
  return (
    <ConvexProvider client={convex}>
      <Component {...pageProps} />
    </ConvexProvider>
  );
}

export default App;

Track Blog Views

Implement view tracking in your blog post component:
BlogPost.tsx
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { useEffect } from "react";

function BlogPost({ slug }: { slug: string }) {
  const viewCount = useQuery(api.views.getViewCount, { slug });
  const incrementView = useMutation(api.views.incrementView);

  useEffect(() => {
    // Increment view count when post is loaded
    incrementView({ slug });
  }, [slug, incrementView]);

  return (
    <div>
      <h1>My Blog Post</h1>
      <p>Views: {viewCount ?? 0}</p>
      {/* Your blog content */}
    </div>
  );
}

export default BlogPost;
The useEffect hook will increment views on every component mount. Consider implementing view throttling or unique visitor tracking to prevent inflated counts.

Display All Blog Stats

Create a query to fetch all blog post views:
convex/views.ts
export const getAllViews = query({
  handler: async (ctx) => {
    return await ctx.db.query("blogViews").collect();
  },
});
Use it in your blog index:
BlogList.tsx
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";

function BlogList() {
  const allViews = useQuery(api.views.getAllViews);

  return (
    <div>
      {allViews?.map((view) => (
        <div key={view.slug}>
          <h3>{view.slug}</h3>
          <p>{view.count} views</p>
        </div>
      ))}
    </div>
  );
}

export default BlogList;

Environment Variables

NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
The Convex URL is automatically provided when you run npx convex dev. Copy it from the terminal output.

Deployment

1

Deploy Convex Functions

Deploy your database functions to production:
npx convex deploy
This command:
  • Pushes your schema and functions to Convex
  • Generates production API credentials
  • Provides your production Convex URL
2

Set Production Environment Variables

Add the production Convex URL to your hosting platform (Netlify, Vercel, etc.):
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
3

Deploy Your Site

Deploy your frontend with the updated environment variables. Convex will automatically handle the connection.

Database Indexes

The by_slug index optimizes queries by blog post slug:
.index("by_slug", ["slug"])

Why Indexes Matter

  • Performance: Enables O(log n) lookups instead of O(n) scans
  • Scalability: Critical as your blog grows
  • Cost Efficiency: Reduces function execution time
Always create indexes for fields you frequently query. Convex automatically uses the most efficient index available.

Type Safety

Convex generates TypeScript types automatically. Import them in your frontend:
import { api } from "../convex/_generated/api";
import type { Doc, Id } from "../convex/_generated/dataModel";

// Type-safe document reference
type BlogView = Doc<"blogViews">;

// Type-safe ID
type BlogViewId = Id<"blogViews">;

Advanced Features

Pagination

Implement pagination for large datasets:
convex/views.ts
export const paginatedViews = query({
  args: {
    paginationOpts: paginationOptsValidator,
  },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("blogViews")
      .order("desc")
      .paginate(args.paginationOpts);
  },
});

Real-time Subscriptions

Convex queries are reactive by default. Components automatically update when data changes:
// This component re-renders whenever the view count changes
const viewCount = useQuery(api.views.getViewCount, { slug });

Scheduled Functions

Reset view counts weekly (example):
convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

crons.weekly(
  "reset weekly views",
  { dayOfWeek: "monday", hourUTC: 0, minuteUTC: 0 },
  internal.views.resetWeeklyViews
);

export default crons;

Best Practices

1

Use Indexes Wisely

Create indexes for all fields you query frequently. Check query performance in the Convex dashboard.
2

Implement Rate Limiting

Prevent view count inflation by throttling increments:
// Only increment once per session
const hasViewed = sessionStorage.getItem(`viewed-${slug}`);
if (!hasViewed) {
  incrementView({ slug });
  sessionStorage.setItem(`viewed-${slug}`, "true");
}
3

Monitor Usage

Track your database usage in the Convex dashboard to stay within free tier limits.
4

Validate Input

Convex automatically validates function arguments based on your schema. Always define argument validators.

Troubleshooting

Connection Issues

Error: Failed to connect to Convex
Solution: Verify your CONVEX_URL environment variable is set correctly and restart your dev server.

Schema Mismatch

Error: Schema validation failed
Solution: Run npx convex dev to sync your schema changes. Clear your browser cache if issues persist.

Missing Generated Files

Error: Cannot find module '../convex/_generated/api'
Solution: Run npx convex dev to generate type definitions. Ensure convex.json points to the correct directory.

Monitoring and Analytics

Access the Convex dashboard for:
  • Real-time Logs: Function execution logs and errors
  • Usage Metrics: Database reads, writes, and storage
  • Function Performance: Execution time and frequency
  • Data Browser: Query and modify data directly
Visit dashboard.convex.dev to access these features.

Build docs developers (and LLMs) love