Skip to main content
SEMastery
Fundamentalsbeginner

How to Sync Google and GitHub Logins to Your Database With Neon Auth for Free

Learn how Neon Auth syncs Google and GitHub logins into your Postgres database for free, with no webhooks, and how to join the data in ASP.NET Core EF Core.

12 min readUpdated September 28, 2025

Imagine you join a new library in your town. At the front desk, a kind librarian writes your name, your card number, and the date you joined into the big register. You did not have to copy your details into every shelf and every room yourself. You just showed your ID once, and the library kept a neat record of you in one trusted place.

Neon Auth works in almost the same way. When a person signs in to your app with their Google or GitHub account, Neon quietly writes that person's basic details into a special table inside your own database. You do not write the copying code. You do not build webhooks. The "librarian" does it for you, and your app can simply read the register whenever it needs to.

In this article we will learn, in very simple steps, how this works and how to use it from a .NET app. We will see what Neon Auth is, how the magic neon_auth.users_sync table gets filled, and how to join that table to your own data in ASP.NET Core with Entity Framework Core. By the end you will be able to let users log in with Google and GitHub, and have their profiles sitting safely in your Postgres database, for free.

The old, painful way

Before tools like Neon Auth, syncing users was a real headache. Your login provider (the service that checks the Google or GitHub password) lived in one place. Your database lived in another place. You had to glue them together yourself.

The usual plan looked like this. The auth provider would send a "webhook" (a small message) every time a user signed up or changed their name. Your server had to catch that message, understand it, and write the user into your database. If the message was lost, your data was wrong. If two messages arrived at once, you got duplicates. If the user changed their email, you had to handle that too.

The old way: you write and maintain the webhook glue yourself.

That webhook code is boring to write, easy to get wrong, and annoying to keep alive. Every team rebuilds it, and every team hits the same bugs. Neon Auth removes this whole job.

What is Neon Auth?

Neon is a serverless Postgres database in the cloud. "Serverless" just means you do not manage a server; you create a database in a few clicks and pay only for what you use, with a generous free plan.

Neon Auth is a feature built on top of Neon. It connects an authentication provider directly to your Neon database. When users log in or update their accounts, the changes flow straight into your database. There is no custom code and no separate webhook for you to babysit.

Here is the key idea. The full, official copy of the user profile lives in the auth provider. Neon Auth keeps a near real-time mirror of that profile inside a table in your database called neon_auth.users_sync. You read from this mirror like any other table.

What Neon Auth does for you

Login
Provider
Sync
Your DB

Steps

1

Login

User clicks Google or GitHub

2

Provider

Auth provider checks identity

3

Sync

Neon mirrors the profile

4

Your DB

Row appears in users_sync

One login, three automatic effects, zero glue code.

The magic table: neon_auth.users_sync

When you turn on Neon Auth, it creates a new schema in your database called neon_auth. Inside it sits a table named users_sync. This table is the register from our library story. Every signed-in user shows up here.

The table has a small, friendly set of columns. You do not create them; Neon does.

ColumnMeaningCan be empty?
idThe unique id of the userNo
emailThe user's main email addressNo
nameThe display nameYes
raw_jsonThe full profile as raw JSONNo
created_atWhen the user first signed upNo
updated_atWhen the profile last changedYes
deleted_atWhen the user was deletedYes

A few things are worth understanding here.

The id is a text value, not a number. Think of it like a membership card number that the auth provider hands out. You will use this id to link your own data to the user.

The raw_json column holds the entire profile as JSON. If you ever need a field that does not have its own column, like a profile picture URL, you can dig it out of raw_json.

The deleted_at column is special. Neon Auth uses soft deletes. When a user is removed, the row is not erased. Instead, Neon writes the date into deleted_at. The row stays so your old data does not suddenly break. To show only active users, you simply ignore the rows where deleted_at has a value.

The lifecycle of one user row inside users_sync.

Step 1: Create a Neon project

First, go to the Neon website and create a free account. Then create a new project. A project comes with a ready-to-use Postgres database and a connection string. Keep that connection string safe; your .NET app will use it to talk to the database.

Inside the Neon Console you will see an Auth tab. Open it. Neon will guide you to enable Neon Auth for your project. With one click, Neon creates the neon_auth schema and the users_sync table in your database.

The nice part for learners: Google sign-in is enabled by default using shared development credentials. That means you can test a Google login straight away without setting up your own OAuth keys. GitHub and other providers can be added when you are ready.

Step 2: Let users log in

Neon Auth gives you a small frontend starter (for example a Next.js template) that already knows how to talk to the auth provider. You run it, open it in your browser, and you will see "Sign in with Google" and "Sign in with GitHub" buttons.

When a user clicks one of those buttons, the OAuth flow runs. OAuth is the polite handshake where Google or GitHub confirms "yes, this is really them" without your app ever seeing their password. After the handshake, the user is signed in, and Neon Auth writes their profile into users_sync.

The OAuth login flow, end to end.

After you log in once, go back to the Neon Console and open the neon_auth.users_sync table. You will see your own row sitting there, filled in automatically. That is the whole point: no webhook, no copy code, just data.

Step 3: Read the table from .NET

Now the fun part for us .NET developers. We want our ASP.NET Core app to read the users_sync table and join it to our own data.

There is one rule to remember. You should never run EF Core migrations against the users_sync table. Neon owns that table. Your app only reads it. If you let your migrations touch it, you could break the sync. So we map it as a read-only entity and tell EF Core to leave it alone.

First, install the Postgres provider for EF Core.

// In your terminal, from the project folder:
// dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
 
// Then map the table with a simple class.
public class NeonUser
{
    public string Id { get; set; } = default!;
    public string Name { get; set; } = default!;
    public string Email { get; set; } = default!;
    public string RawJson { get; set; } = default!;
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
    public DateTime? DeletedAt { get; set; }
}

Next, configure this entity in your DbContext. We point it at the right schema and table, map the snake_case column names, and most importantly call ExcludeFromMigrations() so EF Core never tries to create or change this table.

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options) { }
 
    public DbSet<NeonUser> Users => Set<NeonUser>();
    public DbSet<ProductCart> Carts => Set<ProductCart>();
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<NeonUser>(b =>
        {
            // Neon owns this table; we only read it.
            b.ToTable("users_sync", "neon_auth", t => t.ExcludeFromMigrations());
            b.HasKey(u => u.Id);
            b.Property(u => u.Id).HasColumnName("id");
            b.Property(u => u.Name).HasColumnName("name");
            b.Property(u => u.Email).HasColumnName("email");
            b.Property(u => u.RawJson).HasColumnName("raw_json");
            b.Property(u => u.CreatedAt).HasColumnName("created_at");
            b.Property(u => u.UpdatedAt).HasColumnName("updated_at");
            b.Property(u => u.DeletedAt).HasColumnName("deleted_at");
        });
    }
}

Now you can query users like any other table. Remember to skip soft-deleted users by filtering out rows where DeletedAt is not null.

// Get all active users (ignore soft-deleted ones).
var activeUsers = await db.Users
    .Where(u => u.DeletedAt == null)
    .OrderBy(u => u.Name)
    .ToListAsync();
 
// Find one user by their Neon id.
var me = await db.Users
    .FirstOrDefaultAsync(u => u.Id == userId);

Your app has its own data: carts, orders, posts, comments, and so on. You want each of these to belong to a user. You do that by storing the user's Id (the text id from users_sync) on your own entity.

Here is a ProductCart that belongs to a user. Notice that UserId is a string, because the Neon user id is text.

public class ProductCart
{
    public Guid Id { get; set; }
    public string UserId { get; set; } = default!; // matches NeonUser.Id
    public NeonUser User { get; set; } = default!;
    public decimal Total { get; set; }
}

You can wire the relationship in OnModelCreating. This lets you load a cart together with its user in one query.

modelBuilder.Entity<ProductCart>(b =>
{
    b.HasKey(c => c.Id);
    b.HasOne(c => c.User)
        .WithMany()
        .HasForeignKey(c => c.UserId)
        .OnDelete(DeleteBehavior.Cascade);
});

How your tables connect to users_sync

users_sync
UserId
Your table
Query

Steps

1

users_sync

Holds id, name, email

2

UserId

Text key on your row

3

Your table

Cart, order, or post

4

Query

Join and read together

Your data points at the Neon user id; Neon keeps that id fresh.

A word about foreign keys and the small delay

Remember that the sync is asynchronous. There can be a tiny delay, usually under a second, between a user signing in and their row appearing in users_sync. This matters when you choose how strict to be.

If you add a hard foreign key from your table to users_sync, and you try to save a cart for a user whose row has not synced yet, the save could fail. For most apps this is fine because the user reads their profile first. But if you expect to write data the very instant a user appears, consider a softer link.

You also need to pick the right ON DELETE behavior, because Neon uses soft deletes. Here is a simple guide.

Your dataSuggested ON DELETEWhy
Personal data like a cart or preferencesCascadeIf the user is gone, this private data can go too
Content like blog posts or commentsSetNullThe content should stay even if the author leaves
Audit or history recordsNo FK, keep the raw idHistory must never disappear

Why this is great for beginners

Let us pause and see what we got for free. We never wrote webhook code. We never built a users table by hand. We never worried about a lost sync message creating a duplicate user. We let Neon be the careful librarian, and we just read the register.

This also keeps your code clean. Your app focuses on its real job, like carts and orders, while identity is handled by a service that is built for it. And because everything lives in one Postgres database, your joins are normal SQL joins. No extra API calls to a separate user service.

One last tip: when you need a field that is not a column, reach into raw_json. Postgres can read JSON, and EF Core can query it too. For example, you can pull the avatar URL out of the JSON without adding a new column.

// Read a field from raw_json using PostgreSQL JSON access.
var avatars = await db.Users
    .Where(u => u.DeletedAt == null)
    .Select(u => new
    {
        u.Name,
        Avatar = EF.Functions.JsonValue(u.RawJson, "$.profile_image_url")
    })
    .ToListAsync();

Quick recap

  • Neon Auth syncs users from Google and GitHub into your own Postgres database, with no webhooks and no copy code.
  • The data lands in a table called neon_auth.users_sync that Neon creates and fills for you.
  • Key columns are id, email, name, raw_json, created_at, updated_at, and deleted_at.
  • Neon uses soft deletes: filter out rows where deleted_at is set to show only active users.
  • In .NET, map the table as a read-only EF Core entity and call ExcludeFromMigrations() so your migrations never touch it.
  • Link your own tables (carts, orders, posts) to the user by storing the text Id from users_sync.
  • The sync is slightly delayed (usually under a second), so choose your foreign keys and ON DELETE rules with care.
  • Google sign-in works out of the box on Neon's free plan, so you can start learning today.

References and further reading

Related Posts