As an older developer that is used to the LAMP stack getting used to the new Javascript Frameworks was a bit challenging. One thing that I had found difficult was finding documentation on building out Auth and Crud functionality for the Sveltekit Framework, which is the latest fast kid on the block that is smashing it as far as page load goes compared to more established JS Frameworks.

(source: https://www.softermii.com/blog/why-sveltejs-is-the-most-in-demand-framework-for-web-development )
Do to sveltekit being a new kid on the block there aren’t many established code examples or tutorials on how to do such simple things as CRUD and Auth. I found a nice Auth code base on github.com but it didn’t have a walk through of the code base or any CRUD, so the following article is on how to add CRUD to this very nice Auth open source codebase using sveltekit.
To begin you will need to have some things set up on your development computer, such as Postgresql for the database. Here are the following steps to get things set up.
How to extend Sveltekit-Auth
- download the code base from https://github.com/delay/sveltekit-auth
- install postgres on your system if you don’t have it yet, for osx
> brew install postgresql
> brew services start postgresql@14 or psql postgres
for windows see https://www.postgresql.org/download/windows/
3. In the postgres cli:
CREATE USER your_new_username WITH PASSWORD 'your_password';
4. CREATE DATABASE auth_demo;
5. add permissions to the database for your postgres username:
GRANT ALL PRIVILEGES ON DATABASE auth_demo TO auth_user;
6. open a new cl window or terminal and go to the directory you downloaded sveltekit-auth
>cd to svelte-kit-master dir
7. get the db ready, run the migration
> npx drizzle-kit generate:pg
8. push to the db
> npx drizzle-kit push:pg
If you need to make changes to the schema, you can do so and then run the generate and push commands again to update the database.
9. in the root of the codebase directory copy sample.env to .env file
10. update settings in .env file
# rename to .env and put your values in
# General
# I used postgresql for the PRISMA_URL for this project
# but you should be able to use any DB prisma supports.
# https://www.prisma.io/docs/reference/database-reference/supported-databases
DATABASE_URL="postgresql://postgres_user:user_password@localhost:5432/db_name"
# Good video on how to set up OAuth with Google https://www.youtube.com/watch?v=KfezTtt2GsA
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Email
FROM_EMAIL = 'first last <user@domain.com>'
# use blank values in AWS variables if you want to use SMTP
#AWS SES KEYS
AWS_ACCESS_KEY_ID= ''
AWS_SECRET_ACCESS_KEY= ''
AWS_REGION= '' # us-east-1
AWS_API_VERSION= '' # 2010-12-01
# if AWS SES not set the SMTP will be a fallback
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_SECURE=0 # use 1 for secure
SMTP_USER=somethinghere
SMTP_PASS=somepassword
# Logging
# Clear these to fallback to console.log
AXIOM_TOKEN=your-axiom-token
AXIOM_ORG_ID=your-axiom-org-id
AXIOM_DATASET = your-axiom-dataset
11. sign up at axiom, npm install @axiomhq/js, create a dataset at axiom, and an api token then add to .env file variables for axiom. see https://jeffmcmorris.medium.com/awesome-logging-in-sveltekit-6afa29c5892c
12. install svelte app
> npm install
13. start the sveltekit server
> npm run dev
you should now see a user interface when you open http://localhost:5173 in my case with branding added for my project it looks like this:

Extending the Codebase and adding CRUD Functionality
now that the app is up and running we can start to add functionality that allows one to add db tables and add, edit and delete records to these new tables, essential to any application.
As an example I am going to use creating a settings module for my project. One may want to review the ORM used for db related management, in this case we are using drizzle, https://orm.drizzle.team/docs/get-started-postgresql
there is a drizzle.config.ts file that defines where drizzle schemas are kept. The schema defines the table for the ORM.
see /lib/server/database/drizzle.schemas.ts
add a settings schema to the existing schemas already in that file for user and sessions, in this case one for settings:
export const settingsTable = pgTable('settings', {
id: text('id').notNull().primaryKey(),
user_id: text('user_id')
.notNull()
.references(() => userTable.id),
key: text('key').notNull(),
secret: text('secret').notNull(),
createdAt: timestamp('created_at', {
withTimezone: true,
mode: 'date'
}).notNull(),
updatedAt: timestamp('updated_at', {
withTimezone: true,
mode: 'date'
}).notNull()
});
we are going to be working primarily with the key/secret pair which is used for any standard api calls, in this case an api for getting stock data.
then add a type for settings in the same file and an update type:
export type Settings = typeof settingsTable.$inferInsert;
export type UpdateSettings = Partial<typeof settingsTable.$inferInsert>;
then you need to generate and migrate the changes:
> npx drizzle-kit generate:pg
after you generate there will be a new migration file that is numbered sequentially in ascending order in /lib/server/database/0000_snapshot.json, 0001_snapshot.json
then push changes using:
> npx drizzle-kit push:pg
in the cl or terminal window you should see this:
> auth % npx drizzle-kit generate:pg
drizzle-kit: v0.20.14
drizzle-orm: v0.29.3
No config path provided, using default ‘drizzle.config.ts’
Reading config file ‘/Users/michaelmccarron/Desktop/dev/auth/drizzle.config.ts’
3 tables
sessions 3 columns 0 indexes 1 fks
settings 6 columns 0 indexes 1 fks
users 13 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ src/lib/server/database/migrations/0001_gigantic_quasimodo.sql 🚀
> auth % npx drizzle-kit push:pg
drizzle-kit: v0.20.14
drizzle-orm: v0.29.3
No config path provided, using default path
Reading config file ‘/Users/michaelmccarron/Desktop/dev/auth/drizzle.config.ts’
[✓] Changes applied
before you run this if you went to the postgres cl and did a list tables you see this,
auth_demo=# \dt
public | sessions | table | auth_user
public | users | table | auth_user
then after you do generate and push you see
auth_demo=# \dt
public | sessions | table | auth_user
public | settings | table | auth_user
public | users | table | auth_user
also note that you have a new sql file generated with the additional table information in /src/lib/server/database/migrations/0001_gigantic_quasimodo.sql
CRUD functions are handled in src/lib/server/database/settings-model.ts, this is where you add the functionality for the database model.
export const updateSetting = async (id: string, settings: UpdateSettings) => {
const result = await db.update(settingsTable).set(settings).where(eq(settingsTable.id, id)).returning();
if (result.length === 0) {
return null;
} else {
return result[0];
}
};
export const createSetting = async (settings: Settings) => {
console.log("settings in createSetting of settings-model.ts");
console.log(settings);
const result = await db.insert(settingsTable).values(settings).onConflictDoNothing().returning();
if (result.length === 0) {
return null;
} else {
return result[0];
}
};
User Interface for Settings
settings is manipulated in the UI at localhost:port/profile/settings/
you can download the files for the user interface at:
https://feirmeoirsonrai.me/settings.zip
this should be copied into the /src/routes/(protected)/profile directory so we get /src/routes/(protected)/profile/settings/…
in this example we are going to add a typical key and api string for any api type saas setting.
the eventual directory structure will be like this:
src
— routes
— (protected)
— profile
+page.svelte
+page.server.ts
— settings
+page.svelte
+page.server.ts
— editor
+page.svelte
+page.server.ts
— lister
+page.svele
+page.server.ts
you place files you do not want to be exposed to the user without a valid session id and their session id matching their user.session information, i.e. they own the content in the interface. these parts of your UI are placed in the (protected) directory.
step one is to create a profile directory under (protected)
past settings dir into (protected)/profile/
create file /lib/config/zod-schemas-settings.js
paste into file:
mport { z } from 'zod';
export const settingsSchema = z.object({
id: z.string().optional(),
user_id: z.string().optional(),
key: z
.string({ required_error: 'key is required' })
.min(1, { message: 'key is required' })
.trim(),
secret: z
.string({ required_error: 'secret is required' })
.min(1, { message: 'secret is required' })
.trim(),
createdAt: z.date().optional(),
updatedAt: z.date().optional()
});
export type SettingsSchema = typeof settingsSchema;
after registering as a user update db since not setting up email protocol yet:
UPDATE users SET verified = ‘t’ where email = ‘reg_user@domain.com’;
restart server after updating
npm run dev
When we go to the dashboard section of the site:

work with two files dashboard/+page.server.ts which initializes things for the view.
important files to require are:
import { db } from '$lib/server/db';
import { settingsTable } from '$lib/server/database/drizzle-schemas';
these files are used for accessing the database. because in this view we are performing a data check to see if there is a record for the user we need the drizzle-schemas. db is the equivalent of the DB object in older systems based on PHP, or other backend code bases.
to perform a query run:
const results = await db.select().from(settingsTable).where(eq(settingsTable.user_id, user_id));
+page.svelte is the User interface file. One important require file is that of a zod schema, which is used for data validattion in the form that is included in the file.
import { settingsSchema } from '$lib/config/zod-schemas-settings';
const deleteSettingsSchema = settingsSchema.pick({
id: true,
key: true,
secret: true
});
section: working with editing data and updating the db
if you don’t have any keys then the dashboard provides a link to add a key, this brings up profile/settings/

in +page.server.ts make sure you have any db and db.schema requirements met. in this case since not reading any data or accessing the db, they are not.
in +page.svelte we then include the db schema for writing and the validation zod schemas:
import * as Form from '$lib/components/ui/form';
import { settingsSchema } from '$lib/config/zod-schemas-settings';
import type { SuperValidated } from 'sveltekit-superforms';
once the reqs are loaded then in the ui we see a form with this code:
<div class="flex items-center justify-center mx-auto max-w-2xl">
<Form.Root let:submitting let:errors method="POST" {form} schema={createSettingsSchema} let:config>
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title class="text-2xl">Create Alpaca Trading Keys</Card.Title>
<Card.Description
>You need a API key/secret from https://alpaca.markets</Card.Description>
</Card.Header>
<Card.Content class="grid gap-4">
{#if errors?._errors?.length}
<Alert.Root variant="destructive">
<AlertCircle class="h-4 w-4" />
<Alert.Title>Error</Alert.Title>
<Alert.Description>
{#each errors._errors as error}
{error}
{/each}
</Alert.Description>
</Alert.Root>
{/if}
<Form.Field {config} name="key">
<Form.Item>
<Form.Label>Alpaca Key</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Item>
</Form.Field>
<Form.Field {config} name="secret">
<Form.Item>
<Form.Label>Alpaca Secret</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Item>
</Form.Field>
</Card.Content>
<Card.Footer>
<Form.Button class="w-full" disabled={submitting}
>{#if submitting}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Please wait{:else}Update Keys{/if}
</Form.Button>
</Card.Footer>
</Card.Root>
</Form.Root>
</div>
zod tutorial video: https://www.youtube.com/watch?v=L6BE-U3oy80
once the submit button is pushed after the data is validated using zod schema then the data is inserted into the database as long as the settings schema is defined correctly.
Once we have a setting submitted we can then list the settings.
Listing the Settings and Editing
in dir /profile/settings/lister
we do the usual requirements with the addition of one important new piece of code that handles form actions, such as update, or delete. Here we are only concerned with deleting so we add this to the file +server.page.ts:
export const actions = {
delete: async ({request}) => {
const formData = await request.formData();
const rec_id = formData.get('id');
console.log("rec id "+rec_id);
console.log('deleting key');
const deletedSetting = await deleteSetting(rec_id);
console.log("deletedSetting "+deletedSetting);
if (deletedSetting) {
console.log("deleted succeessful");
setFlash(
{
type: 'success',
message: 'keys deleted.'
},
request
);
redirect(302, '/dashboard');
//message(request, { text: 'deleted!' });
}
}
};
this is called from +page.svelte
we need to add some new code to the file:
const deleteSettingsSchema = settingsSchema.pick({
id: true,
key: true,
secret: true
});
type DeleteSettingsSchema = typeof deleteSettingsSchema;
export let form: SuperValidated<DeleteSettingsSchema>;
then in the user interface we can iterate through the settings record rows, although here we are limited to only one:
<ul>
{#each data.json_results as alpaca}
<li>{alpaca.key} - {alpaca.secret} <a href="/profile/settings/editor/{alpaca.id}">[edit]</a>
<form method="POST" onsubmit="return confirm('sure you want to delete this key?');" action="?/delete">
<input type="hidden" id="id" name="id" value="{alpaca.id}" />
<button>Delete</button>
</form>
</li>
--------- <br />
{/each}
</ul>
Editing the Settings

Next we move onto editing. If one were to see in the previous code that there is a link to editing. This is where we use a new directory [id] which is at /profile/settings/editor/[id]
see https://svelte.dev/docs/kit/advanced-routing for more information on how this works but basically we are passing in a dynamic ‘id’ variable to display a user interface based on the dynamic argument.
for editing we are working with the files in the /profile/settings/editor/[id]/
in page.server.ts we need to make sure we require the schemas for settings update and other functionality:
import { editSetting } from '$lib/server/database/settings-model';
import { updateSetting } from '$lib/server/database/settings-model';
import { settingsTable } from '$lib/server/database/drizzle-schemas';
and again we need to add the actions for editing:
export const actions = {
default: async (event) => {
const form = await superValidate(event, keySchema);
if (!form.valid) {
return fail(400, {
form
});
}
//add user to db
try {
console.log('updating profile');
const user = event.locals.user;
if (user) {
await updateSetting(event.params.id, {
key: form.data.key,
secret: form.data.secret
});
setFlash({ type: 'success', message: 'Keys update successful.' }, event);
}
} catch (e) {
console.error(e);
return setError(form, 'There was a problem updating your trading keys.');
}
console.log('keys updated successfully');
return message(form, 'keys updated successfully.');
}
};
in +page.svelte we make sure we have schema for validation:
import { settingsSchema } from '$lib/config/zod-schemas-settings';
import type { SuperValidated } from 'sveltekit-superforms';
and we need:
const keySchema = settingsSchema.pick({
key: true,
secret: true,
});
type keySchema = typeof keySchema;
export let form: SuperValidated<keySchema>;
form = data.form;
and then we have the form itself:
<div class="flex items-center justify-center mx-auto max-w-2xl">
<Form.Root let:submitting let:errors method="POST" {form} schema={keySchema} let:config>
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title class="text-2xl">Edit Alpaca Trading Keys</Card.Title>
<Card.Description
>update your Alpaca Keys</Card.Description>
</Card.Header>
<Card.Content class="grid gap-4">
{#if errors?._errors?.length}
<Alert.Root variant="destructive">
<AlertCircle class="h-4 w-4" />
<Alert.Title>Error</Alert.Title>
<Alert.Description>
{#each errors._errors as error}
{error}
{/each}
</Alert.Description>
</Alert.Root>
{/if}
<Form.Field {config} name="key">
<Form.Item>
<Form.Label>Alpaca Key</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Item>
</Form.Field>
<Form.Field {config} name="secret">
<Form.Item>
<Form.Label>Alpaca Secret</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Item>
</Form.Field>
</Card.Content>
<Card.Footer>
<Form.Button class="w-full" disabled={submitting}
>{#if submitting}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Please wait{:else}Update Key/Secret{/if}
</Form.Button>
</Card.Footer>
</Card.Root>
</Form.Root>
</div>
the values for the fields, is from form.data object. note that the name of the form field matches the value of the form.data.field_name
for example:
form.data.key should be referenced <Form.Field {config} name=”key”></Form.Field>
And that is how one adds CRUD to sveltekit code base for this project: sveltekit-auth, as seen adapted for my project Céillí.
Leave a Reply