https://github.com/ntalbotdev/answerly-nuxt
Answerly - Q&A social app (Nuxt + Supabase)
https://github.com/ntalbotdev/answerly-nuxt
nuxt pinia postgresql scss supabase tailwindcss typescript vercel
Last synced: 2 months ago
JSON representation
Answerly - Q&A social app (Nuxt + Supabase)
- Host: GitHub
- URL: https://github.com/ntalbotdev/answerly-nuxt
- Owner: ntalbotdev
- Created: 2025-07-08T23:23:05.000Z (12 months ago)
- Default Branch: main
- Last Pushed: 2025-07-16T08:05:51.000Z (11 months ago)
- Last Synced: 2025-07-17T02:07:55.641Z (11 months ago)
- Topics: nuxt, pinia, postgresql, scss, supabase, tailwindcss, typescript, vercel
- Language: Vue
- Homepage: https://answerly-nuxt.vercel.app
- Size: 532 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Answerly Nuxt App
A robust Nuxt 4 CRUD application leveraging Supabase for authentication, database management, and profile asset storage, Pinia for state handling, and Tailwind CSS for modern UI styling. Testing is provided via Vitest for unit tests and Playwright for end-to-end automation.
## Features
- Supabase Auth (email/password)
- Email verification on signup
- Password reset functionality
- User profile creation and management
- Public profile pages
- Profile editing and assets upload (avatar/banner via Supabase Storage)
- Ask questions to any user (optionally anonymously)
- Anonymous questions protect user privacy and show as "Anonymous" in notifications
- Users can answer questions they receive
- Questions are only published after being answered
- Follow/unfollow users (social feature)
- View followers and following lists for any user
- See mutual follow status on profiles
- Real-time notification system with automatic cleanup
- Get notified when someone follows you
- Get notified when someone asks you a question
- Get notified when someone answers your question
- Notifications are automatically removed when actions are completed (questions answered/deleted, users unfollowed)
- Real-time updates using Supabase subscriptions
- Mark notifications as read (delete from system)
- Clear all notifications at once
- Real-time inbox system
- Questions appear instantly when received
- Real-time updates when questions are answered or deleted
- Pinia for state management
- Middleware for route protection and redirects
- See the full [TODO list](./TODO.md) for upcoming features and improvements
## Project Structure
- `app/` — Main application source folder (Nuxt 4 standard)
- `assets/` — Static assets
- `components/` — Reusable Vue components
- `composables/` — Reusable composable functions
- `layouts/` — Nuxt layouts
- `middleware/` — Route guards and redirects
- `pages/` — Nuxt pages (routes)
- `profile/[username]/` — Profile pages using shared components
- `stores/` — Pinia stores (profile, questions, notifications)
- `utils/` — Utility functions and constants
- `scripts/` — Scripts for automation and development tasks
- `tests/` — Unit and end-to-end tests
- `e2e/` — End-to-end tests using Playwright
- `unit/` — Unit tests using Vitest
## Environment Setup
1. **Environment Variables**
- Create a `.env` file in the project root with:
```
SUPABASE_URL=your-supabase-url
SUPABASE_ANON_KEY=your-supabase-anon-key
```
2. **Nuxt Modules**
- `@nuxtjs/supabase`
- `@pinia/nuxt`
- `@nuxtjs/tailwindcss`
- `@nuxt/test-utils/module`
- `@nuxt/eslint`
- `@nuxt/icon`
3. **Install dependencies**
```
npm install
```
## Supabase Setup
### Enable Auth & Database
- Go to [Supabase](https://app.supabase.com/)
- Create a new project (Supabase Database/Postgres)
- Enable email/password authentication in the Auth settings
### Database Schema
#### `profiles` Table
| Column | Type | Description |
| ------------ | ----------- | -------------------------------- |
| user_id | uuid | Primary key, Default: auth.uid() |
| username | text | Unique, required |
| display_name | text | Nullable, user display name |
| avatar_url | text | Nullable, profile picture URL |
| banner_url | text | Nullable, profile banner URL |
| bio | text | Nullable, user bio |
| created_at | timestamptz | Default: now() |
| updated_at | timestamptz | Default: now() |
📄 Profiles Table SQL Query
```sql
create table profiles (
user_id uuid primary key default auth.uid() on delete cascade,
username text unique not null,
display_name text,
avatar_url text,
banner_url text,
bio text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
```
#### `follows` Table
| Column | Type | Description |
| ------------ | ----------- | ----------------------------------------- |
| follower_id | uuid | Primary key, references profiles(user_id) |
| following_id | uuid | Primary key, references profiles(user_id) |
| created_at | timestamptz | Default: now() |
📄 Follows Table SQL Query
```sql
create table follows (
follower_id uuid references profiles(user_id) on delete cascade,
following_id uuid references profiles(user_id) on delete cascade,
created_at timestamptz not null default now(),
primary key (follower_id, following_id)
);
```
#### `questions` Table
| Column | Type | Description |
| ------------ | ----------- | ------------------- |
| id | uuid | Primary key |
| from_user_id | uuid | From user, required |
| to_user_id | uuid | To user, required |
| question | text | Required |
| is_anonymous | boolean | Default: false |
| answer | text | Nullable |
| published | boolean | Default: false |
| created_at | timestamptz | Default: now() |
| answered_at | timestamptz | Nullable |
📄 Questions Table SQL Query
```sql
create table questions (
id uuid primary key default gen_random_uuid(),
from_user_id uuid not null references profiles(user_id) on delete cascade,
to_user_id uuid not null references profiles(user_id) on delete cascade,
question text not null,
is_anonymous boolean not null default false,
answer text,
published boolean not null default false,
created_at timestamptz not null default now(),
answered_at timestamptz
);
```
#### `notifications` Table
| Column | Type | Description |
| ------------ | ----------- | ----------------------- |
| id | uuid | Primary key |
| user_id | uuid | User ID, required |
| type | text | Notification type |
| payload | jsonb | Nullable, Flexible data |
| created_at | timestamptz | Default: now() |
| event_id | text | Generated from payload |
📄 Notifications Table SQL Query
```sql
create table notifications (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references profiles(user_id) on delete cascade,
type text not null check (type in ('follow', 'question', 'answer', 'system')),
payload jsonb,
created_at timestamptz not null default now(),
event_id text generated always as (
case
when type = 'follow' then COALESCE(payload::jsonb->>'follower_id', '') || ':' || COALESCE(payload::jsonb->>'following_id', '')
when type = 'question' then COALESCE(payload::jsonb->>'question_id', '')
when type = 'answer' then COALESCE(payload::jsonb->>'question_id', '')
else COALESCE(id::text, '')
end
) stored,
unique (user_id, type, event_id)
);
```
### Edge Functions
🔧 send-notification Edge Function
```ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req) => {
const origin = req.headers.get("origin") || "*";
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "POST, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
}
});
}
const supabaseUrl = Deno.env.get("SUPABASE_URL");
const supabaseServiceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
if (!supabaseUrl || !supabaseServiceRoleKey) {
return new Response("Missing environment variables", {
status: 500,
headers: {
"Access-Control-Allow-Origin": origin
}
});
}
const supabase = createClient(supabaseUrl, supabaseServiceRoleKey);
if (req.method === "DELETE") {
const { user_id, event_id, type } = await req.json();
if (!user_id || !event_id || !type) {
return new Response("Missing required fields for deletion", {
status: 400,
headers: {
"Access-Control-Allow-Origin": origin
}
});
}
const { error } = await supabase.from("notifications")
.delete()
.eq("user_id", user_id)
.eq("event_id", event_id)
.eq("type", type);
if (error) {
return new Response(`Error deleting notification: ${error.message}`, {
status: 500,
headers: {
"Access-Control-Allow-Origin": origin
}
});
}
return new Response("Notification deleted", {
status: 200,
headers: {
"Access-Control-Allow-Origin": origin
}
});
}
const { user_id, type, payload } = await req.json();
if (!user_id || !type) {
return new Response("Missing required fields", {
status: 400,
headers: {
"Access-Control-Allow-Origin": origin
}
});
}
const { data, error } = await supabase.from("notifications").upsert([
{
user_id,
type,
payload
}
], {
onConflict: [
"user_id",
"type",
"event_id"
]
}).select();
if (error) {
return new Response(`Error: ${error.message}`, {
status: 500,
headers: {
"Access-Control-Allow-Origin": origin
}
});
}
return new Response("Notification inserted", {
status: 200,
headers: {
"Access-Control-Allow-Origin": origin
}
});
});
```
## Storage & Profile Assets
- Each user can upload an avatar and a banner image to their own folder: `[user_id]/avatar.webp` and `[user_id]/banner.webp`
- The `avatar_url` and `banner_url` fields in the profile point to the public URLs of the uploaded images
- Make the bucket public for public URLs
- Use the RLS policies below to restrict access to profile assets
## Row Level Security (RLS)
> ℹ️ Click the spoilers below to reveal the SQL queries for each policy
📦 Storage RLS Policies
```sql
CREATE POLICY "Public can view avatar/banner"
ON storage.objects
FOR SELECT
TO public
USING (
bucket_id = 'profile-assets'
AND (
name LIKE '%/avatar.webp'
OR name LIKE '%/banner.webp'
)
);
CREATE POLICY "Users can upload avatar/banner to their folder"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'profile-assets'
AND (
name = (select auth.uid())::text || '/avatar.webp'
OR name = (select auth.uid())::text || '/banner.webp'
)
);
CREATE POLICY "Users can update avatar/banner in their folder"
ON storage.objects
FOR UPDATE
TO authenticated
USING (
bucket_id = 'profile-assets'
AND (
name = (select auth.uid())::text || '/avatar.webp'
OR name = (select auth.uid())::text || '/banner.webp'
)
)
WITH CHECK (
bucket_id = 'profile-assets'
AND (
name = (select auth.uid())::text || '/avatar.webp'
OR name = (select auth.uid())::text || '/banner.webp'
)
);
CREATE POLICY "Users can delete avatar/banner from their folder"
ON storage.objects
FOR DELETE
TO authenticated
USING (
bucket_id = 'profile-assets'
AND (
name = (select auth.uid())::text || '/avatar.webp'
OR name = (select auth.uid())::text || '/banner.webp'
)
);
```
👤 Profiles RLS Policies
```sql
CREATE POLICY "Public can view profiles"
ON profiles
FOR SELECT
TO public
USING (true);
CREATE POLICY "Users can create their own profile"
ON profiles
FOR INSERT
TO authenticated
WITH CHECK (
user_id = (select auth.uid())
);
CREATE POLICY "Users can update their own profile"
ON profiles
FOR UPDATE
TO authenticated
USING (
user_id = (select auth.uid())
)
WITH CHECK (
user_id = (select auth.uid())
);
```
🤝 Follows RLS Policies
```sql
CREATE POLICY "Everyone can view follows"
ON follows
FOR SELECT
TO public
USING (true);
CREATE POLICY "Users can follow others"
ON follows
FOR INSERT
TO authenticated
WITH CHECK (
follower_id = (select auth.uid())
);
CREATE POLICY "Users can unfollow others"
ON follows
FOR DELETE
TO authenticated
USING (
follower_id = (select auth.uid())
);
```
❓ Questions RLS Policies
```sql
CREATE POLICY "Public can view published questions"
ON questions
FOR SELECT
TO public
USING (
published = true
);
CREATE POLICY "Users can view their own questions"
ON questions
FOR SELECT
TO authenticated
USING (
from_user_id = (select auth.uid())
OR to_user_id = (select auth.uid())
);
CREATE POLICY "Users can ask questions"
ON questions
FOR INSERT
TO authenticated
WITH CHECK (
from_user_id = (select auth.uid())
);
CREATE POLICY "Users can answer questions sent to them"
ON questions
FOR UPDATE
TO authenticated
USING (
to_user_id = (select auth.uid())
)
WITH CHECK (
to_user_id = (select auth.uid())
);
CREATE POLICY "Users can delete questions they asked or received"
ON questions
FOR DELETE
TO authenticated
USING (
from_user_id = (select auth.uid())
OR to_user_id = (select auth.uid())
);
```
🔔 Notifications RLS Policies
```sql
CREATE POLICY "Users can view their notifications"
ON notifications
FOR SELECT
TO authenticated
USING (
user_id = (select auth.uid())
);
CREATE POLICY "Users can update their notifications"
ON notifications
FOR UPDATE
TO authenticated
USING (
user_id = (select auth.uid())
)
WITH CHECK (
user_id = (select auth.uid())
);
CREATE POLICY "Users can delete their notifications"
ON notifications
FOR DELETE
TO authenticated
USING (
user_id = (select auth.uid())
);
CREATE POLICY "Allow system inserts for notifications"
ON notifications
FOR INSERT
TO service_role
WITH CHECK (true);
```
## Usage
- Sign up and log in with email/password (needs email verification)
- After signup, a profile is created in the `profiles` table
- Visit `/` as a guest to see the welcome page; as a logged-in user, you'll see your feed
- Visit `/inbox` to answer questions sent to you (only published after answering)
- Real-time updates when new questions arrive
- Answering or deleting questions automatically removes related notifications
- Visit `/notifications` to see real-time notifications for user activity and events
- Follow notifications: See who followed you
- Question notifications: See new questions you received
- Answer notifications: See when your questions are answered
- Click "Mark as read" to permanently delete individual notifications
- Use "Clear all" to remove all notifications at once
- Visit `/my-questions` to see questions you have asked others
- Visit `/profile/:username` to view a public profile (ex: [/profile/axile](https://answerly-nuxt.vercel.app/profile/axile))
- If it's your own profile, you can edit it by clicking the edit button
- Follow/unfollow users with automatic notification management
- Visit `/profile/:username/followers` to see a user's followers
- Visit `/profile/:username/following` to see who a user is following
## Development
```bash
npm run dev
```
## Testing
- Run unit tests using Vitest:
```bash
npm run test
```
- Run end-to-end tests using Playwright:
```bash
npm run test:e2e
```
- This will generate `tests/e2e/auth.json` with authenticated user session data and open Playwright in UI mode.
- Use Playwright codegen to generate tests:
```bash
npm run codegen:e2e
```
- This will open a browser window where you can interact with the app and record your actions.
### Test User
- Email: `test@test.com`
- Password: `test123`
---
**Built with Nuxt, Supabase, Pinia, and Tailwind CSS.**