-
-
Notifications
You must be signed in to change notification settings - Fork 500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Remove adapter packages #1639
Comments
...and by version 7, Lucia will just be a README on how to setup auth. 😅 (jk) This makes a lot of sense to me. |
This makes a lot of sense, previously at work I had to rewrite the drizzle adapter, which was a little pain because of the typing, just to change the users queries to query a junction table along with it. This suggested approach would've made that job a lot easier, although it might steep up the learning curve for inexperienced developers that aren't that comfortable with SQL yet, but even that could be a good thing if the docs has a basic guide/example on how to build adapters, creating an opportunity for them to learn. |
My experience with the Drizzle adapter has been fine so far but I appreciate this change, having more control over how the queries work would be great. |
Would providing some adapters for "simple"/"base" uses makes sense? Maybe not in the core Lucia, but under different packages? If I need just a "quick" user authentication with Drizzle without any "customization," could we still have the adapter? To avoid copying/pasting eventual examples? But I agree with whoever came before me: having more control is very nice and allows devs to have more control, even if there is a little bit of a cost in terms of speed. |
@lutefd Fortunately the adapter code is going to be significantly simpler for ORM users. Like for Prisma: const result = await this.sessionModel.findUnique({
where: {
id: sessionId
},
include: {
user: true
}
);
if (result === null) {
return { session: null, user: null };
}
const { user, ...session } = result; |
@Giggiux For Prisma and Drizzle maybe, though I think it'll be just easier to copy-paste the code from the docs. Unlike the SQL drivers, you won't need to edit the code for a basic setup. |
Consider creating an init command like This command would plop in place the fully written out auth code, allowing the dev to focus more on the app while retaining the access to edit the adapter code if need be. Similar to how shadcn/ui does UI components, could be done for auth. |
For Prisma, this will just work with full type inference. I'm guessing something similar is possible with drizzle import type { Session, User } from "@prisma/client";
import type { DatabaseAdapter, SessionAndUser } from "lucia";
const adapter: DatabaseAdapter<Session, User> = {
getSessionAndUser: async (
sessionId: string
): Promise<SessionAndUser<Session, User>> => {
const result = await client.session.findUnique({
where: {
id: sessionId,
},
include: {
user: true,
},
});
if (result === null) {
return { session: null, user: null };
}
const { user, ...session } = result;
return { session, user };
},
deleteSession: async (sessionId: string): Promise<void> => {
await client.session.delete({
where: {
id: sessionId,
},
});
},
updateSessionExpiration: async (
sessionId: string,
expiresAt: Date
): Promise<void> => {
await client.session.update({
where: {
id: sessionId,
},
data: {
expiresAt,
},
});
},
}; |
This makes a lot of sense, it's so much better to empower users to create their own database queries, wether through an ORM or not and have total control over that aspect of things. Giving users a clear API on how to build an adapter is a much better approach and I'm very happy with the proposal! One of side effect of using Lucia is having ownership and clarity over the whole auth process and I believe this is another step in that direction. |
@mirorauhala I've considered that on numerous occasions but I'm not sure how well that'd work. I'd love to see a PoC tho. |
@pilcrowonpaper I think the drizzle one will be pretty similar to what you've shared, the current way already works well like this, so I guess it should work in the new way with complete type inference, the snippet you proposed looks way cleaner as well. export class DrizzleSQLiteAdapter implements Adapter {
private db: BaseSQLiteDatabase<any, any, typeof schema>;
private sessionTable: SQLiteSessionTable;
private userTable: SQLiteUserTable;
constructor(
db: BaseSQLiteDatabase<any, any, any>,
sessionTable: SQLiteSessionTable,
userTable: SQLiteUserTable
) {
this.db = db;
this.sessionTable = sessionTable;
this.userTable = userTable;
}
async deleteSession(sessionId: string): Promise<void> {
await this.db
.delete(this.sessionTable)
.where(eq(this.sessionTable.id, sessionId));
}
async deleteUserSessions(userId: string): Promise<void> {
await this.db
.delete(this.sessionTable)
.where(eq(this.sessionTable.userId, userId));
}
async getSessionAndUser(
sessionId: string
): Promise<[session: DatabaseSession | null, user: DatabaseUser | null]> {
const result = await this.db.query.sessionTable.findFirst({
where: eq(this.sessionTable.id, sessionId),
with: {
user: {
with: {
usersToRoles: {
with: {
role: true,
},
},
},
},
},
});
if (!result?.user) return [null, null];
const session = {
id: result.id,
userId: result.userId,
expiresAt: result.expiresAt,
is_oauth: result.is_oauth,
};
return [
transformIntoDatabaseSession(session),
transformIntoDatabaseUser(result.user),
];
} Building on what @mirorauhala suggested, you'd need to have it prompt the db that's being used to properly set it up, as well if there's an ORM being used and which if so, that would require to have base adapters to all the major DBs, as sometimes the typing changes between them (eg. SQLite), and supported ORMs. It should work but it seems like a ton of work and a bit of a headache to maintain without heavy community support. Shadcn/ui works because it's already assuming you're using react and from the folder Layout it can detect if it's using Next to add the "use client" directives or not. With auth there's a lot more variables and it seems more complex to set it up, but it's a great idea nonetheless if it had more people interested in maintaining it. |
I think this makes a tonne of sense. Lucia already gives you a lot of control compared to other Auth libraries so I think users of it will not be put off by writing the database implementation themselves and will probably appreciate it, if anything. I think it also makes sense because if a library like Drizzle suddenly ships a breaking change then the Drizzle adapter library suddenly has to change too or pin a peer dependency that could stop users of Drizzle from updating, not great either way. If decent examples were provided on how to set it up that can be copied and pasted into your code base then I'm sure that'd be a great starting point. |
@lutefd Not entirely correct, the |
@ERmilburn02 Yeah, I've just simplified because that isn't really the point, I never said it needs to, only that it can, usually if you're doing a init that need to be iterated on you'll need to write a config file, And even that is in most cases a insertion of the "use client" directive. It detects if it's Next.js because as of now it's the only major framework to use RSC extensively, but when dealing with auth there would be a lot of other variables than that, even in Next.js land the way the oauth callbacks are written would differ if it were written for the Pages router or the App router. Outside of that it would be different for Astro, Sveltekit, Nuxt, etc.. It would also need to have different base inserts, due to session creation, for some dbs due to their column typing, so it would need to prompt that, as it would need to create a base db adapter, if you're using an ORM on top of that it would need to have a different template for each of those frameworks and the list goes on, not even stoping to consider different runtime environments, like vercel's edge, hono, node, which all have their particularities (like edge couldn't really handle drizzle base connection to postgres out of the box last time I've checked), that seems like a nightmare to handle for each Provider as suggested, of course there could always be limits and starting with only a few, but even then it would be complicated. It would be a large project that should be outside of the main Lucia project scope, even if you limit which frameworks and environments, don't get me wrong it's a great idea and maybe lucia's new design could really make it possible, but it would still require a lot of thought and work. JS land is just a nightmare. |
This looks like a great direction and I welcome this proposed change. |
This makes a lot of sense so we have maximum flexibility. I would love to see in the docs the changes about how V3 did things and how you can do the same in V4 and maybe where you can start doing your own things. Then its perfect |
I'm assuming this would also empower the user to implement their own Redis session cache, which was somewhat of a struggle to do in V3. |
@hansaskov yes! it'll be significantly easier to build adapters compared to v3 |
Having built an adapter for Lucia, I think this is a great move. I think my project would better as some copy/paste code from a README or blog to encourage others to control how Lucia works, rather than provide a package that needs to be maintained and potentially not live up to everyone's needs/wants/demands. |
I think the trade off is definitely worth it - I like the changes! |
You can see the full implementation on the https://github.com/lucia-auth/lucia/tree/v4-remove-adapter-package |
Fully support this. Just this week I spent 4 hours debugging (yes, yes, a bit of a skill issue) before I realized that Lucia wasn't working because my postgres.js connection the adapter was using included I think clear and explicit is better than obfuscated and implicit, even if it's more verbose at first, as long as that verbosity is well encapsulated (as it is in your example). It's easier for people new to Lucia to grasp what Lucia is doing, and once people are comfortable with that then they can hide the verbosity in a way that suits them. I switched to Lucia from Supabase, after spending days trying to resolve this supabase/supabase-js#1010, among some other issues. AFAIK, Supabase STILL hasn't resolved this beyond suppressing the warning, and in general Supabase is far more sequential database calls than necessary too - a performance killer if your backend isn't in the same datacenter as your Supabase instance. If I wanted a "do everything for me and keep me reliant on the service" package I'd have stayed with Supabase or similar. I'd suggest that if you're trying to make money with Lucia as as service, you eventually have to go the route of trying to do everything for customers to make it as one click as possible, and that will only grow the abstractions and difficulty necessary to turn everything into a one-liner of a function call. If you're not going down that route, clearing away the high costs (to you) of abstractions in favor of returning control to devs yields solid performance and maintenance benefits for everyone at a small cost of requiring devs to manually write queries. And if the devs wanting to use Lucia cannot or will not manually write queries even with Claude, Copilot, or ChatGPT, then is Lucia the right tool for them in the first place? Edit: Worded closing question more appropriately. |
Also agree with this change, especially as we have a little unusual db schema I think so the defaults wouldn't really work for us and a good point @daniellovera made (I think) is "you probably shouldn't use Lucia (or similar packages or even roll your own auth) if you can't write those queries, much safer to go with a full-blown service or start (really) understanding the necessary queries / Lucia. |
Here's a quick demo using Drizzle ORM https://github.com/pilcrowOnPaper/lucia-remove-adapter-package-demo There are no docs but you can play around with the implementation with |
One thing I'm not 100% sure about is whether we should have |
I think this makes a lot of sense, that way we can manage our own database schema/queries however we want and just pass on data from lucia. Basically lucia becomes a series of utility classes/functions but with a clear set of suggested guidelines for how to build your own auth. It sounds quite good. It almost makes the documentation harder to write than the code itself which is not a bad thing 😀 |
Having the freedom in every aspect of how to implement every single Lucia aspect makes total sense. So yes query our database ourself, structure it the way we want and let lucia know how we structured it to make it all work is perfect |
adapters is what made me choose lucia, I agree that they don't need to be in core but instead just make them separate packages. |
This should also prevent the gotcha of having an attribute named |
With adapters separated from this library and Looking at the current V4, it seems like letting the developer decide if there's a interface Session {
id: string;
expiresAt: Date;
user: User;
} Since the developer now has to build there own queries for session data, is there any reason to enforce the return type of those queries (e.g. requiring a User) and limiting the use cases of the library?
Is the API working with the user in a meaningful way after the changes in V4? It seems like these changes (putting the onus of the queries on the developer) dissociate the Edit: I'm relatively new to using this library, so I don't mean to overstep, but honest question -- with the direction this library is going, it doesn't seem like it has any authentication aspects to it. Of course the underlying libraries you've created have tons of authentication utilities that are insanely helpful (thank you for all your work!!!), but from my new and potentially naive perspective this is looking much more like a session library than an authentication library. Is that the end goal -- for Lucia to be a session management tool and Oslo and Arctic to be utilities for building the authentication methods? Please correct me if I'm completely misguided here! |
I would much rather decide for myself what is the exact shape of that user object and how to query it. Whether Lucia ends up dealing with the user object at all is a good question though. One way of doing this could be to pass arguments to Lucia, one of them being the query function that returns the user object. If you go that route though, I would ask myself why are we also not also passing as arguments the functions that create and return the session objects. |
@jshear This is probably just personal preference, but I don't like nesting the user object in the session object. Like, it doesn't work well if you have a |
Honestly the end goal for me would be for Lucia docs to be a collection of tutorials and examples, much more fleshed out than what we have now. |
Agreed on that, good point. But there are still a few ways to get the best of both worlds I believe. Currently, we have A small change in terminology and minor typing tweaks could make this much more dynamic with no loss in clarity (in my opinion). Instead of having the type To make the implementation match the capabilities of the current version, Not sure exactly what that would mean for If you favor keeping the concept of a User, is there any reason that If this goes against any other design decisions that I'm unaware of just let me know. But I've been getting familiar with this library recently and a change like this is the only thing I would need to use Lucia for all of my session use-cases instead of co-mingling Lucia with other session management depending on whether or not a user is authenticated. Since users are no longer intertwined with any part of the library other than the adapter getter function, I was optimistic that this would be a good time to make a change like this. Appreciate your time and consideration! |
I really like the general direction. Good job! A few suggestions:
export interface User {
id: string;
username: string;
}
export interface Session extends LuciaSession {
user: User;
}
const adapter: DatabaseAdapter<Session> = {
getSession: (sessionId: string): Session => {
const row = db.queryRow(
"SELECT session.id, session.user_id, session.expires_at, session.login_at, user.id, user.username FROM session INNER JOIN user ON user.id = session.user_id WHERE session.id = ?",
[sessionId]
);
if (row === null) {
return null;
}
const session: Session = {
id: row[0],
expiresAt: new Date(row[2] * 1000),
loginAt: new Date(row[3] * 1000),
user: {
id: row[4],
username: row[5],
}
};
return session;
},
createSession(session: Session) {
db.execute(
"INSERT INTO session (id, expires_at, user_id, login_at) VALUES (?, ?, ?)",
[session.id, session.expiresAt, session.user.id, Math.floor(new Date().getTime() / 1000)]
);
return { ... session, loginAt = ... }
}
// other methods as above
}
// in user code
lucia.createSession( {
id: 'custom id'
user: {
id: 'my user'
}
})
// ********
// or with only the user id stored
export interface Session extends LuciaSession {
userId: string;
}
const adapter: DatabaseAdapter<Session> = {
getSession: (sessionId: string): Session => {
const row = db.queryRow(
"SELECT session.id, session.user_id, session.expires_at, session.login_at FROM session WHERE session.id = ?",
[sessionId]
);
if (row === null) {
return null;
}
const session: Session = {
id: row[0],
expiresAt: new Date(row[2] * 1000),
loginAt: new Date(row[3] * 1000),
userId: row[1]
};
return session;
},
createSession(session: Session) {
db.execute(
"INSERT INTO session (id, expires_at, user_id, login_at) VALUES (?, ?, ?)",
[session.id, session.expiresAt, session.userId, Math.floor(new Date().getTime() / 1000)]
);
return { ... session, loginAt = ... }
}
// other methods as above
}
// in user code
const session = lucia.createSession( {
id: 'custom id'
userId: 'my user'
})
const user = db.fetchUser(session.userId)
****
// in lucia core:
public async createSession(
session?: DeepPartial<Session>
): Promise<Session> {
const sessionId = session?.id ?? generateIdFromEntropySize(25);
const sessionExpiresAt = session?.expiresAt ?? createDate(this.sessionExpiresIn);
return await this.adapter.createSession({
...session,
id: sessionId,
expiresAt: sessionExpiresAt,
})
} |
@pilcrowonpaper There hasn't been any update to the |
@lts20050703 "This will be part of Lucia v4, which will probably be released late this year or early next year. It's very possible that v4 will be our last major update." |
Database adapters have been a fundamental part of Lucia from the very start. It allows Lucia to work with any databases and Lucia provides adapter packages to make the integration easier. But is this correct approach?
I've... actually never considered that. It just made sense when starting, especially since that's how libraries like NextAuth did it. But now I have a definite answer. The adapter model is fine but we shouldn't provide any built-in adapters.
The ready-made adapters are a pain to work with and has the domino effect of making Lucia core bloated. From the start, it's been a struggle make each adapter flexible while keeping the API simple. Take for example the many SQL adapters. We have to dynamically create prepared statements based on custom attributes and escape the table and column names. And it affects the core as well. I don't think anybody likes
getUserAttributes()
anddefine module
. These API had to be included to provide flexibility and type-inference. But is it worth it? We added all these layers of abstractions so that so devs don't have to manually write queries, but really that's probably the easiest thing for web devs to do.I'm now convinced that it's a bad idea to create libraries around database drivers and ORMs if you want to provide any sort of flexibility.
So if Lucia doesn't write your queries for you, what can the next version of Lucia look like?
The biggest and obvious change is that you have to manually define your adapter. I've used a SQL driver as an example so it's a bit verbose (also see the very clean Prisma adapter), but the queries aren't that complicated at all. We could add sample code for popular drivers and ORMs in the docs, which would make the setup easier too.
From an API design perspective, I think this is beautiful. For starters, minimal generic usage and config options. But I really love the most is that
LuciaSession
is just an interface that your custom session object needs to satisfy. No moregetSessionAttributes()
ordeclare module
funkiness. This also means that discriminated unions just works for both sessions and usersFor creating session, just write a single query. We'll provide the utilities for generating IDs and calculating expirations.
At this point, why keep the user handling? I've thought about this for quite a while, and the simple answer is that it's better to have an API that works with one thing (user + session) rather than two things (user + session or session). If you just need any more flexible than this - just build your own. It's likely that whatever API we provide won't be enough. I think it's fine for Lucia to have some opinions and this feels like the place to draw the line.
On a final note, this could allow us to bring back framework-specific APIs. I don't have the appetite to deal with the mess of JS frameworks, but maybe :D
This will be part of Lucia v4, which will probably be released late this year or early next year. It's very possible that v4 will be our last major update.
The text was updated successfully, but these errors were encountered: