The platform started with one side – users making plans, tagging friends, joining each other’s plans. The graph model handles that well. Users connect through KNOWS edges, plans connect through CREATED, JOINED, TAGGED.
Now we’re adding the other side. Businesses. A cafe with a slow Tuesday wants to drop a deal into the plans forming around it. A venue can offer a discount to a group that’s already half-organized. The product idea from the beginning was that both sides would share the same surface – people with half-formed plans and businesses with empty hours, meeting in the same feed.
The question was whether the graph model would hold up when we added a completely different type of actor.
Same shape, different entry point
When I started building the business model, I expected it to be a different kind of code. It isn’t.
A user creating a plan:
(:User) -[:created]-> (:Plan)
A business offering a deal:
(:Business) -[:offered]-> (:Deal)
The pattern is identical. A node creates a relationship to another node. The traversal to fetch “all deals from this business” is the same shape as “all plans from this user”:
START owner=node({businessId})
MATCH (owner) -[:offered]-> (deal)
WHERE deal.created_at > {timestamp}
RETURN deal, owner
ORDER BY deal.created_at DESC
Swap offered for created, deal for plan, businessId for userId – same query. The graph doesn’t care which side you enter from.
The middleware trick
On the user side, every authenticated route runs through a middleware chain that checks the session, validates the user against the database, and preloads data. The business side needs the same thing but for business accounts.
Instead of duplicating the chain, we added one middleware function:
mw.businessMode = function(req, res, next) {
req.isBusiness = res.locals.isBusiness = true;
req.pathPrefix = res.locals.pathPrefix = '/business';
next();
};
It flips a flag. The existing auth and validate middleware already check req.isBusiness to decide which user model to query – regular or business. So the business route chain is:
var bizChain = [mw.businessMode, mw.auth, mw.validate];
app.get('/business', bizChain, offer.get, business.index);
One flag, same chain, different user type. The deal routes follow the same pattern as plan routes:
app.post('/business/offer', mw.auth,
offer.collect, offer.validate,
offer.create, offer.respond, offer.errors);
Validate, create, respond, handle errors. Same pipeline as plans.
What the graph gives you
The interesting part isn’t that the code is similar – you can write similar-looking code against any database. The interesting part is that both sides live in the same graph.
A plan has a location. A deal has a location. A plan has attendees. A deal has attendees. When we eventually build the query that connects plans to nearby deals, it’ll be a graph traversal – walk from plans to their locations, then to deals at the same locations. One query, no joins across separate tables, no coordination between services.
We’re not there yet. Right now the user feed and the business dashboard are separate views. But the data is already in the right shape. Users, plans, businesses, deals – all nodes in the same graph, all connected through typed edges. The traversal that links them is waiting to be written.
The part that wasn’t obvious
One thing tripped us up. Plans have a when field – the scheduled time – and a created_at – when the plan was posted. The feed sorts by created_at. But deals have a different relationship with time. A deal is valid for a window, not scheduled for a moment. We ended up overloading when to mean “valid until” for deals and “happening at” for plans. Same field name, different semantics. It works but I’m not proud of it.
The other thing: a business querying “show me plans happening near my venue this weekend” is the query that ties the two sides together. We haven’t built it yet. The data is there – plans have locations, businesses have locations – but the traversal needs spatial awareness that Neo4j doesn’t really have right now.
The accidental pattern
I didn’t plan for the two sides to be this symmetric. I built the user model first, then copied the pattern for businesses because it was the fastest way to ship. Jyoti noticed it before I did – she was wiring up the business dashboard views and said the templates were almost identical to the user ones. Two people, one codebase, no time to invent a second architecture. The symmetry just happened.
