diff --git a/scripts/fix-coordinates.ts b/scripts/fix-coordinates.ts
index 32b30b6..031e000 100644
--- a/scripts/fix-coordinates.ts
+++ b/scripts/fix-coordinates.ts
@@ -42,7 +42,7 @@ const COORDS: Record<string, { lat: number; lng: number; region?: string }> = {
 // Fix coordinates
 let fixed = 0;
 const updated = REACT_COMMUNITIES.map(comm => {
-  if (comm.coordinates.lat === 0 && comm.coordinates.lng === 0) {
+  if (!comm.coordinates || (comm.coordinates.lat === 0 && comm.coordinates.lng === 0)) {
     const coords = COORDS[comm.city];
     if (coords) {
       fixed++;
diff --git a/scripts/merge-all-communities.ts b/scripts/merge-all-communities.ts
index 0ede1d3..daefdbb 100644
--- a/scripts/merge-all-communities.ts
+++ b/scripts/merge-all-communities.ts
@@ -100,7 +100,7 @@ async function main() {
     const key = normalizeName(comm.name);
 
     // Fix coordinates if missing
-    if (comm.coordinates.lat === 0 || comm.coordinates.lng === 0) {
+    if (!comm.coordinates || comm.coordinates.lat === 0 || comm.coordinates.lng === 0) {
       const cityCoords = CITY_COORDINATES[comm.city] || CITY_COORDINATES[`${comm.city}, ${comm.country}`];
       if (cityCoords) {
         comm.coordinates = { lat: cityCoords.lat, lng: cityCoords.lng };
diff --git a/src/app/admin/data/communities/page.tsx b/src/app/admin/data/communities/page.tsx
index b67d2e7..5e6343d 100644
--- a/src/app/admin/data/communities/page.tsx
+++ b/src/app/admin/data/communities/page.tsx
@@ -5,9 +5,12 @@
 
 import { getRedisClient } from '@/lib/redis';
 import { getCommunities, migrateFromOldFormat } from '@/lib/redis-communities';
+import { getSubmissions } from '@/lib/redis-community-submissions';
+import type { CommunitySubmission } from '@/types/community-submission';
 import { RFDS } from '@/components/rfds';
 import { MigrationButton } from './migration-button';
 import { CommunitiesTable } from '@/components/admin/CommunitiesTable';
+import { PendingSubmissionsTable } from '@/components/admin/PendingSubmissionsTable';
 
 export const dynamic = 'force-dynamic';
 
@@ -40,15 +43,24 @@ async function getCommunitiesData() {
     const countries = new Set(communities.map(c => c.country)).size;
     const totalMembers = communities.reduce((sum, c) => sum + (c.member_count || 0), 0);
 
+    let pendingSubmissions: CommunitySubmission[] = [];
+    try {
+      const submissions = await getSubmissions();
+      pendingSubmissions = submissions.filter(s => s.verification_status === 'pending');
+    } catch (error) {
+      console.error('Failed to fetch submissions:', error);
+    }
+
     return {
       communityKeys: communityKeys.length,
       totalCommunities: communities.length,
       activeCommunities,
       countries,
       totalMembers,
-      communities, // Include for display
+      communities,
       storageFormat,
       needsMigration: oldFormatExists && !indexExists,
+      pendingSubmissions,
     };
   } catch (error) {
     console.error('Error fetching communities data:', error);
@@ -114,6 +126,16 @@ export default async function CommunitiesPage() {
         </div>
       </div>
       
+      {/* Pending Submissions */}
+      {data.pendingSubmissions.length > 0 && (
+        <div className="bg-card border border-warning/50 rounded-xl p-6">
+          <h3 className="text-lg font-bold text-foreground mb-4">
+            Pending Submissions ({data.pendingSubmissions.length})
+          </h3>
+          <PendingSubmissionsTable submissions={data.pendingSubmissions} />
+        </div>
+      )}
+
       {/* Communities Table */}
       {data.communities && data.communities.length > 0 && (
         <div className="bg-card border border-border rounded-xl p-6">
diff --git a/src/app/api/admin/community-submissions/approve/route.ts b/src/app/api/admin/community-submissions/approve/route.ts
new file mode 100644
index 0000000..e9b700f
--- /dev/null
+++ b/src/app/api/admin/community-submissions/approve/route.ts
@@ -0,0 +1,91 @@
+/**
+ * Approve Community Submission API
+ * POST /api/admin/community-submissions/approve
+ *
+ * Converts a pending submission into a full Community in Redis.
+ */
+
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { UserManagementService } from '@/lib/admin/user-management-service';
+import { getSubmissionById, approveSubmissionAtomic } from '@/lib/redis-community-submissions';
+import type { Community } from '@/types/community';
+
+function slugify(name: string): string {
+  const slug = name
+    .toLowerCase()
+    .replace(/[^a-z0-9]+/g, '-')
+    .replace(/^-|-$/g, '');
+  if (!slug) throw new Error(`Could not generate slug from name: "${name}"`);
+  return slug;
+}
+
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    if (!session?.user?.email) {
+      return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
+    }
+
+    const isAdmin = await UserManagementService.isAdmin(session.user.email);
+    if (!isAdmin) {
+      return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
+    }
+
+    const { id } = await request.json();
+    if (!id) {
+      return NextResponse.json({ error: 'Submission ID required' }, { status: 400 });
+    }
+
+    const submission = await getSubmissionById(id);
+    if (!submission) {
+      return NextResponse.json({ error: 'Submission not found' }, { status: 404 });
+    }
+
+    const now = new Date().toISOString();
+    const communityId = `community-${crypto.randomUUID()}`;
+    const community: Community = {
+      id: communityId,
+      name: submission.name,
+      slug: slugify(submission.name),
+      city: submission.city,
+      country: submission.country,
+      region: submission.region,
+      timezone: submission.timezone ?? 'UTC',
+      coordinates: submission.coordinates,
+      organizers: [
+        {
+          id: `org-${crypto.randomUUID()}`,
+          name: submission.organizer_name,
+          role: 'Lead Organizer',
+        },
+      ],
+      founded_date: now,
+      event_types: ['meetup'],
+      website: submission.website,
+      meetup_url: submission.meetup_url,
+      description: submission.description,
+      member_count: submission.member_count ?? 0,
+      typical_attendance: 0,
+      meeting_frequency: 'monthly',
+      primary_language: 'English',
+      status: 'new',
+      invite_only: false,
+      verified: true,
+      verification_status: 'verified',
+      cois_tier: 'none',
+      created_at: now,
+      updated_at: now,
+    };
+
+    await approveSubmissionAtomic(id, community);
+
+    return NextResponse.json({ success: true, communityId });
+  } catch (error: unknown) {
+    const message = error instanceof Error ? error.message : 'Failed to approve';
+    const status = message.includes('Concurrent') || message.includes('already processed') ? 409 : 500;
+    console.error('Error approving submission:', error);
+    return NextResponse.json({ error: message }, { status });
+  }
+}
diff --git a/src/app/api/admin/community-submissions/reject/route.ts b/src/app/api/admin/community-submissions/reject/route.ts
new file mode 100644
index 0000000..b5549a6
--- /dev/null
+++ b/src/app/api/admin/community-submissions/reject/route.ts
@@ -0,0 +1,46 @@
+/**
+ * Reject Community Submission API
+ * POST /api/admin/community-submissions/reject
+ */
+
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { UserManagementService } from '@/lib/admin/user-management-service';
+import { getSubmissionById, updateSubmission } from '@/lib/redis-community-submissions';
+
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    if (!session?.user?.email) {
+      return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
+    }
+
+    const isAdmin = await UserManagementService.isAdmin(session.user.email);
+    if (!isAdmin) {
+      return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
+    }
+
+    const { id, reason } = await request.json();
+    if (!id) {
+      return NextResponse.json({ error: 'Submission ID required' }, { status: 400 });
+    }
+
+    const submission = await getSubmissionById(id);
+    if (!submission) {
+      return NextResponse.json({ error: 'Submission not found' }, { status: 404 });
+    }
+
+    await updateSubmission(id, {
+      verification_status: 'rejected',
+      reviewed_by: session.user.email,
+      reviewed_at: new Date().toISOString(),
+      rejection_reason: reason || undefined,
+    });
+
+    return NextResponse.json({ success: true });
+  } catch (error: unknown) {
+    console.error('Error rejecting submission:', error);
+    return NextResponse.json({ error: 'Failed to reject' }, { status: 500 });
+  }
+}
diff --git a/src/app/api/admin/community-submissions/route.ts b/src/app/api/admin/community-submissions/route.ts
new file mode 100644
index 0000000..8dc5d37
--- /dev/null
+++ b/src/app/api/admin/community-submissions/route.ts
@@ -0,0 +1,32 @@
+/**
+ * List Community Submissions API
+ * GET /api/admin/community-submissions
+ */
+
+import { NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { UserManagementService } from '@/lib/admin/user-management-service';
+import { getSubmissions } from '@/lib/redis-community-submissions';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    if (!session?.user?.email) {
+      return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
+    }
+
+    const isAdmin = await UserManagementService.isAdmin(session.user.email);
+    if (!isAdmin) {
+      return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
+    }
+
+    const submissions = await getSubmissions();
+    return NextResponse.json({ submissions });
+  } catch (error: unknown) {
+    console.error('Error fetching submissions:', error);
+    return NextResponse.json({ error: 'Failed to fetch submissions' }, { status: 500 });
+  }
+}
diff --git a/src/app/api/communities/submit/route.ts b/src/app/api/communities/submit/route.ts
index 2bd7073..9e3fcff 100644
--- a/src/app/api/communities/submit/route.ts
+++ b/src/app/api/communities/submit/route.ts
@@ -6,10 +6,11 @@
 import { NextResponse } from 'next/server';
 import { getServerSession } from 'next-auth';
 import { authOptions } from '@/lib/auth';
+import { saveSubmission } from '@/lib/redis-community-submissions';
+import type { CommunitySubmission } from '@/types/community-submission';
 
 export async function POST(request: Request) {
   try {
-    // Verify authentication
     const session = await getServerSession(authOptions);
 
     if (!session?.user?.email) {
@@ -21,7 +22,6 @@ export async function POST(request: Request) {
 
     const body = await request.json();
 
-    // Validate required fields
     const required = ['name', 'address', 'city', 'country', 'description', 'organizer_name', 'organizer_email'];
     for (const field of required) {
       if (!body[field]) {
@@ -32,9 +32,23 @@ export async function POST(request: Request) {
       }
     }
 
-    // Create submission record
-    const submission = {
-      id: `submission-${Date.now()}`,
+    const maxLengths: Record<string, number> = {
+      name: 200, address: 500, city: 100, country: 100,
+      description: 2000, organizer_name: 200, organizer_email: 254,
+      meetup_url: 500, website: 500,
+    };
+    for (const [field, max] of Object.entries(maxLengths)) {
+      if (typeof body[field] === 'string' && body[field].length > max) {
+        return NextResponse.json(
+          { success: false, error: `${field} exceeds ${max} characters` },
+          { status: 400 }
+        );
+      }
+    }
+
+    const now = new Date().toISOString();
+    const submission: CommunitySubmission = {
+      id: `submission-${crypto.randomUUID()}`,
       name: body.name,
       address: body.address,
       city: body.city,
@@ -48,25 +62,20 @@ export async function POST(request: Request) {
       submitted_by: session.user.email,
       verification_status: 'pending',
       geocoded: false,
-      submitted_at: new Date().toISOString(),
-      created_at: new Date().toISOString(),
-      updated_at: new Date().toISOString(),
+      submitted_at: now,
+      created_at: now,
+      updated_at: now,
     };
 
-    console.log('📝 New community submission:', submission);
-
-    // TODO: Store in Redis pending queue: pending_communities:{id}
-    // TODO: Geocode address to get coordinates
-    // TODO: Send email notification to admins
-    // TODO: Add to admin review queue
+    await saveSubmission(submission);
 
     return NextResponse.json({
       success: true,
       message: 'Community submitted successfully. We will review and add it soon!',
     });
 
-  } catch (error: any) {
-    console.error('❌ Community submission failed:', error);
+  } catch (error: unknown) {
+    console.error('Community submission failed:', error);
     return NextResponse.json(
       { success: false, error: 'Submission failed' },
       { status: 500 }
diff --git a/src/app/communities/[slug]/page.tsx b/src/app/communities/[slug]/page.tsx
index 9d09e56..2de3035 100644
--- a/src/app/communities/[slug]/page.tsx
+++ b/src/app/communities/[slug]/page.tsx
@@ -282,10 +282,12 @@ export default async function CommunityPage({ params }: CommunityPageProps) {
                 </h3>
                 <div className="space-y-3 text-sm">
                   <InfoRow label="Timezone" value={community.timezone} />
-                  <InfoRow
-                    label="Location"
-                    value={`${community.coordinates.lat.toFixed(4)}, ${community.coordinates.lng.toFixed(4)}`}
-                  />
+                  {community.coordinates && (
+                    <InfoRow
+                      label="Location"
+                      value={`${community.coordinates.lat.toFixed(4)}, ${community.coordinates.lng.toFixed(4)}`}
+                    />
+                  )}
                   {community.founded_date && (
                     <InfoRow
                       label="Founded"
diff --git a/src/components/admin/PendingSubmissionsTable.tsx b/src/components/admin/PendingSubmissionsTable.tsx
new file mode 100644
index 0000000..5cf5de2
--- /dev/null
+++ b/src/components/admin/PendingSubmissionsTable.tsx
@@ -0,0 +1,175 @@
+/**
+ * Pending Community Submissions Table
+ * Shows submissions awaiting admin review with approve/reject actions
+ */
+
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { RFDS } from '@/components/rfds';
+import type { CommunitySubmission } from '@/types/community-submission';
+
+interface PendingSubmissionsTableProps {
+  submissions: CommunitySubmission[];
+}
+
+export function PendingSubmissionsTable({ submissions }: PendingSubmissionsTableProps) {
+  const router = useRouter();
+  const [loadingId, setLoadingId] = useState<string | null>(null);
+  const [rejectingId, setRejectingId] = useState<string | null>(null);
+  const [rejectReason, setRejectReason] = useState('');
+  const [errorId, setErrorId] = useState<string | null>(null);
+
+  async function handleApprove(id: string) {
+    setLoadingId(id);
+    setErrorId(null);
+    try {
+      const res = await fetch('/api/admin/community-submissions/approve', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ id }),
+      });
+      if (!res.ok) throw new Error('Failed to approve');
+      router.refresh();
+    } catch {
+      setErrorId(id);
+    } finally {
+      setLoadingId(null);
+    }
+  }
+
+  async function handleReject(id: string) {
+    setLoadingId(id);
+    setErrorId(null);
+    try {
+      const res = await fetch('/api/admin/community-submissions/reject', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ id, reason: rejectReason }),
+      });
+      if (!res.ok) throw new Error('Failed to reject');
+      setRejectingId(null);
+      setRejectReason('');
+      router.refresh();
+    } catch {
+      setErrorId(id);
+    } finally {
+      setLoadingId(null);
+    }
+  }
+
+  function startReject(id: string) {
+    setRejectingId(id);
+    setRejectReason('');
+    setErrorId(null);
+  }
+
+  function cancelReject() {
+    setRejectingId(null);
+    setRejectReason('');
+  }
+
+  if (submissions.length === 0) {
+    return (
+      <p className="text-sm text-muted-foreground">No pending submissions.</p>
+    );
+  }
+
+  return (
+    <div className="space-y-4">
+      {submissions.map((s) => (
+        <div
+          key={s.id}
+          className="border border-border rounded-lg p-4 bg-muted/30 space-y-3"
+        >
+          <div className="flex items-start justify-between gap-4">
+            <div>
+              <h4 className="font-semibold text-foreground">{s.name}</h4>
+              <p className="text-sm text-muted-foreground">
+                {s.city}, {s.country}
+              </p>
+            </div>
+            <RFDS.SemanticBadge variant={
+              s.verification_status === 'pending' ? 'warning' :
+              s.verification_status === 'rejected' ? 'destructive' : 'success'
+            }>
+              {s.verification_status}
+            </RFDS.SemanticBadge>
+          </div>
+
+          <p className="text-sm text-foreground">{s.description}</p>
+
+          <div className="grid grid-cols-2 gap-2 text-sm text-muted-foreground">
+            <div>Organizer: {s.organizer_name} ({s.organizer_email})</div>
+            <div>Submitted by: {s.submitted_by}</div>
+            {s.website && <div>Website: {s.website}</div>}
+            {s.meetup_url && <div>Meetup: {s.meetup_url}</div>}
+            {s.member_count ? <div>Members: {s.member_count}</div> : null}
+            <div>Submitted: {new Date(s.submitted_at).toLocaleDateString()}</div>
+          </div>
+
+          {errorId === s.id && (
+            <p className="text-sm text-destructive">Action failed. Please try again.</p>
+          )}
+
+          {s.verification_status === 'pending' && (
+            <div className="flex items-center gap-2 pt-2 border-t border-border">
+              {rejectingId === s.id ? (
+                <div className="flex items-center gap-2 flex-1">
+                  <input
+                    type="text"
+                    placeholder="Reason (optional)"
+                    value={rejectReason}
+                    onChange={(e) => setRejectReason(e.target.value)}
+                    className="flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background text-foreground"
+                  />
+                  <RFDS.SemanticButton
+                    variant="destructive"
+                    size="sm"
+                    onClick={() => handleReject(s.id)}
+                    disabled={loadingId === s.id}
+                  >
+                    {loadingId === s.id ? 'Rejecting...' : 'Confirm'}
+                  </RFDS.SemanticButton>
+                  <RFDS.SemanticButton
+                    variant="secondary"
+                    size="sm"
+                    onClick={cancelReject}
+                  >
+                    Cancel
+                  </RFDS.SemanticButton>
+                </div>
+              ) : (
+                <>
+                  <RFDS.SemanticButton
+                    variant="primary"
+                    size="sm"
+                    onClick={() => handleApprove(s.id)}
+                    disabled={loadingId === s.id}
+                  >
+                    {loadingId === s.id ? 'Approving...' : 'Approve'}
+                  </RFDS.SemanticButton>
+                  <RFDS.SemanticButton
+                    variant="destructive"
+                    size="sm"
+                    onClick={() => startReject(s.id)}
+                    disabled={loadingId === s.id}
+                  >
+                    Reject
+                  </RFDS.SemanticButton>
+                </>
+              )}
+            </div>
+          )}
+
+          {s.verification_status === 'rejected' && s.rejection_reason && (
+            <p className="text-sm text-destructive pt-2 border-t border-border">
+              Rejected: {s.rejection_reason}
+            </p>
+          )}
+        </div>
+      ))}
+    </div>
+  );
+}
diff --git a/src/components/communities/CommunityMap.tsx b/src/components/communities/CommunityMap.tsx
index 5123e4b..b771ead 100644
--- a/src/components/communities/CommunityMap.tsx
+++ b/src/components/communities/CommunityMap.tsx
@@ -284,13 +284,13 @@ export function CommunityMap() {
           url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
         />
 
-        {communities.map((community) => {
+        {communities.filter((c) => c.coordinates).map((community) => {
           const iconOptions = createCustomIcon(community.cois_tier, community.status);
 
           return (
             <Marker
               key={community.id}
-              position={[community.coordinates.lat, community.coordinates.lng]}
+              position={[community.coordinates!.lat, community.coordinates!.lng]}
               icon={
                 iconOptions && L
                   ? L.divIcon(iconOptions)
diff --git a/src/lib/redis-community-submissions.ts b/src/lib/redis-community-submissions.ts
new file mode 100644
index 0000000..0070b01
--- /dev/null
+++ b/src/lib/redis-community-submissions.ts
@@ -0,0 +1,144 @@
+/**
+ * Redis Community Submission Storage
+ * Stores pending community submissions for admin review
+ *
+ * Storage:
+ * - Individual submissions: community_submissions:{id} (JSON)
+ * - Index set: community_submissions:index (Redis SET of all IDs)
+ */
+
+import type { CommunitySubmission } from '@/types/community-submission';
+import type { Community } from '@/types/community';
+
+const SUBMISSIONS_INDEX_KEY = 'community_submissions:index';
+const SUBMISSION_KEY_PREFIX = 'community_submissions:';
+
+function getSubmissionKey(id: string): string {
+  return `${SUBMISSION_KEY_PREFIX}${id}`;
+}
+
+async function getRedis() {
+  try {
+    const { getRedisClient } = await import('./redis');
+    return getRedisClient();
+  } catch (error) {
+    console.error('Redis unavailable for community submissions:', error);
+    return null;
+  }
+}
+
+export async function saveSubmission(submission: CommunitySubmission): Promise<void> {
+  const redis = await getRedis();
+  if (!redis) throw new Error('Redis not available');
+
+  const pipeline = redis.pipeline();
+  pipeline.set(getSubmissionKey(submission.id), JSON.stringify(submission));
+  pipeline.sadd(SUBMISSIONS_INDEX_KEY, submission.id);
+  await pipeline.exec();
+}
+
+export async function getSubmissions(): Promise<CommunitySubmission[]> {
+  const redis = await getRedis();
+  if (!redis) return [];
+
+  const ids = await redis.smembers(SUBMISSIONS_INDEX_KEY);
+  if (ids.length === 0) return [];
+
+  const keys = ids.map(id => getSubmissionKey(id));
+  const pipeline = redis.pipeline();
+  keys.forEach(k => pipeline.get(k));
+  const results = await pipeline.exec();
+  const values = results?.map(([, v]) => v as string | null) ?? [];
+
+  return values
+    .filter((v): v is string => v !== null)
+    .map(v => JSON.parse(v) as CommunitySubmission)
+    .sort((a, b) => b.submitted_at.localeCompare(a.submitted_at));
+}
+
+export async function getSubmissionById(id: string): Promise<CommunitySubmission | null> {
+  const redis = await getRedis();
+  if (!redis) return null;
+
+  const data = await redis.get(getSubmissionKey(id));
+  if (!data) return null;
+
+  return JSON.parse(data) as CommunitySubmission;
+}
+
+export async function updateSubmission(
+  id: string,
+  updates: Partial<CommunitySubmission>
+): Promise<CommunitySubmission> {
+  const redis = await getRedis();
+  if (!redis) throw new Error('Redis not available');
+
+  const key = getSubmissionKey(id);
+
+  // WATCH key for optimistic locking — if another request modifies
+  // the key between GET and EXEC, the transaction aborts.
+  await redis.watch(key);
+
+  const existing = await redis.get(key);
+  if (!existing) {
+    await redis.unwatch();
+    throw new Error(`Submission ${id} not found`);
+  }
+
+  const submission = JSON.parse(existing) as CommunitySubmission;
+  const updated: CommunitySubmission = {
+    ...submission,
+    ...updates,
+    id,
+    updated_at: new Date().toISOString(),
+  };
+
+  const result = await redis.multi().set(key, JSON.stringify(updated)).exec();
+  if (!result) {
+    throw new Error(`Concurrent modification on submission ${id}, retry`);
+  }
+
+  return updated;
+}
+
+/**
+ * Atomically add community to Redis + remove submission from queue.
+ * Uses MULTI/EXEC transaction so all ops succeed or none do.
+ */
+export async function approveSubmissionAtomic(
+  submissionId: string,
+  community: Community
+): Promise<void> {
+  const redis = await getRedis();
+  if (!redis) throw new Error('Redis not available');
+
+  const submissionKey = getSubmissionKey(submissionId);
+  await redis.watch(submissionKey);
+
+  const existing = await redis.get(submissionKey);
+  if (!existing) {
+    await redis.unwatch();
+    throw new Error('Submission already processed');
+  }
+
+  const result = await redis.multi()
+    .set(`communities:${community.id}`, JSON.stringify(community))
+    .sadd('communities:index', community.id)
+    .del(submissionKey)
+    .srem(SUBMISSIONS_INDEX_KEY, submissionId)
+    .exec();
+
+  if (!result) {
+    throw new Error('Concurrent approval detected');
+  }
+}
+
+export async function deleteSubmission(id: string): Promise<void> {
+  const redis = await getRedis();
+  if (!redis) throw new Error('Redis not available');
+
+  const pipeline = redis.pipeline();
+  pipeline.del(getSubmissionKey(id));
+  pipeline.srem(SUBMISSIONS_INDEX_KEY, id);
+  await pipeline.exec();
+}
diff --git a/src/types/community.ts b/src/types/community.ts
index 7143105..d162ded 100644
--- a/src/types/community.ts
+++ b/src/types/community.ts
@@ -13,7 +13,7 @@ export interface Community {
   country: string;
   region?: string; // State/Province
   timezone: string;
-  coordinates: {
+  coordinates?: {
     lat: number;
     lng: number;
   };
